Business Rules: Unit 5 — Export & Polish
BR-01: HTML Export
| Rule | Description |
|---|---|
| BR-01.1 | HTML export MUST parse the full source text via swift-markdown and walk the AST to produce HTML — not regex-based conversion |
| BR-01.2 | All GFM elements supported by the editor (headings, bold, italic, strikethrough, code, links, images, lists, task lists, tables, blockquotes, horizontal rules) MUST be represented in HTML output |
| BR-01.3 | Image src attributes MUST preserve the original relative paths from the Markdown source |
| BR-01.4 | HTML text content MUST be entity-escaped (<, >, &, ", ') to prevent injection |
| BR-01.5 | Generated HTML MUST be a complete standalone document with <!DOCTYPE html>, <html>, <head> (with <meta charset="UTF-8">), and <body> |
| BR-01.6 | Generated HTML MUST include embedded CSS styles (not external stylesheet) for portable rendering |
| BR-01.7 | The <title> element MUST be set to the document file name (without .md extension), or “Untitled” for unsaved documents |
| BR-01.8 | Empty source text MUST produce ExportError.emptySource — do not generate an empty HTML file |
| BR-01.9 | Fenced code blocks with a language specifier MUST include a class="language-{lang}" attribute on the <code> element |
| BR-01.10 | Task list items MUST render as <input type="checkbox" disabled> (disabled — not interactive in exported HTML) |
BR-02: PDF Export
| Rule | Description |
|---|---|
| BR-02.1 | PDF export MUST first generate HTML (identical to BR-01 output), then render that HTML to PDF via WKWebView.createPDF(configuration:) |
| BR-02.2 | The WKWebView used for PDF rendering MUST be offscreen (not visible to the user) |
| BR-02.3 | The WKWebView MUST be loaded with baseURL set to the source document’s parent directory so that relative image paths resolve correctly |
| BR-02.4 | PDF rendering MUST wait for WKNavigationDelegate.webView(_:didFinish:) before capturing — do not snapshot mid-load |
| BR-02.5 | PDF rendering MUST enforce a timeout of 30 seconds; if exceeded, throw ExportError.pdfRenderingTimedOut |
| BR-02.6 | Default paper size MUST be A4 (595.28 x 841.89 points) with 72pt (1-inch) margins on all sides |
| BR-02.7 | PDF output MUST preserve the visual appearance of the HTML rendering (fonts, colors, table borders, code block backgrounds) |
| BR-02.8 | The offscreen WKWebView MUST be deallocated after PDF data is captured to avoid memory leaks |
| BR-02.9 | WKWebView operations MUST execute on the main thread (@MainActor) as required by WebKit |
BR-03: Export Dialog
| Rule | Description |
|---|---|
| BR-03.1 | Export MUST present NSSavePanel for the user to choose the destination — do not auto-save to a default location |
| BR-03.2 | NSSavePanel.allowedContentTypes MUST be set to the exported format’s UTType (.html or .pdf) |
| BR-03.3 | The suggested file name MUST be the document name with the appropriate extension (e.g., MyDocument.html) |
| BR-03.4 | The default save directory MUST be the same directory as the source .md file, or the user’s Documents folder for unsaved documents |
| BR-03.5 | If the user cancels the save panel, the export operation MUST be silently abandoned — no error shown |
| BR-03.6 | If file write fails, an NSAlert MUST be presented with the error description from ExportError.fileWriteFailed |
| BR-03.7 | Export MUST NOT block the editor — the user should be able to continue editing while the export runs in the background |
BR-04: Sidebar
| Rule | Description |
|---|---|
| BR-04.1 | The sidebar MUST display recently opened .md files sourced from NSDocumentController.shared.recentDocumentURLs |
| BR-04.2 | Sidebar items MUST be sorted by last opened date, most recent first |
| BR-04.3 | The currently active document MUST be visually distinguished in the sidebar (highlight or accent color) |
| BR-04.4 | Clicking a sidebar item MUST open that document via NSDocumentController.shared.openDocument(withContentsOf:display:) |
| BR-04.5 | Sidebar visibility MUST be toggleable via toolbar button and keyboard shortcut Cmd+Shift+L |
| BR-04.6 | Sidebar visibility state MUST persist across app launches (store in UserDefaults) |
| BR-04.7 | When no recent files exist, the sidebar MUST display a “No Recent Files” placeholder — not an empty blank area |
| BR-04.8 | Sidebar MUST update its list when a document is opened or closed (observe NSDocumentController notifications) |
| BR-04.9 | Sidebar width MUST be constrained to a reasonable range (180-280 points) to prevent it from dominating the window |
| BR-04.10 | File display names MUST omit the .md extension for cleaner presentation |
BR-05: Dark Mode
| Rule | Description |
|---|---|
| BR-05.1 | All UI surfaces (editor, sidebar, toolbar, preferences) MUST adapt to the current effective appearance (light or dark) |
| BR-05.2 | The editor background MUST use semantic colors: white-ish in light mode, dark gray (#1E1E1E) in dark mode |
| BR-05.3 | Editor text MUST use .labelColor (black in light, near-white in dark) — never hardcoded color values |
| BR-05.4 | Syntax highlighting colors in active blocks MUST use NSColor semantic/system colors that resolve dynamically (.secondaryLabelColor, .linkColor, .systemOrange, .systemGreen) |
| BR-05.5 | The sidebar MUST use .windowBackgroundColor for its background and .labelColor / .secondaryLabelColor for text |
| BR-05.6 | Inactive rendered blocks (styled output) MUST use semantic colors — bold text in .labelColor, links in .linkColor, code backgrounds in system-appropriate tint |
| BR-05.7 | Appearance changes MUST trigger a full RenderCache invalidation and re-render of all editor blocks (per Unit 1 BR-05.6) |
| BR-05.8 | The transition between light and dark modes MUST be immediate — no fade animation on the editor content |
BR-06: Appearance Override
| Rule | Description |
|---|---|
| BR-06.1 | EditorSettings.appearanceOverride MUST support three modes: .system, .light, .dark |
| BR-06.2 | .system mode MUST follow the macOS system appearance and update automatically when the user toggles Dark Mode in System Settings |
| BR-06.3 | .light mode MUST force NSApp.appearance = NSAppearance(named: .aqua) regardless of system setting |
| BR-06.4 | .dark mode MUST force NSApp.appearance = NSAppearance(named: .darkAqua) regardless of system setting |
| BR-06.5 | Appearance override MUST be persisted in UserDefaults (key: editor.appearanceOverride) and restored on app launch |
| BR-06.6 | Changing appearance override MUST take effect immediately — no app restart required |
| BR-06.7 | Appearance override applies app-wide — all open document windows MUST reflect the same appearance |
| BR-06.8 | The Preferences UI for appearance selection MUST show a segmented picker or dropdown with the three options, with the current selection highlighted |
Keyboard Shortcuts (Export & Sidebar)
| Shortcut | Action | Notes |
|---|---|---|
| Cmd+Shift+E | Export to HTML | Opens save dialog with .html type |
| Cmd+Shift+P | Export to PDF | Opens save dialog with .pdf type |
| Cmd+Shift+L | Toggle sidebar | Show/hide recent files sidebar |