diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 11:09:02 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-20 13:56:41 -0500 |
| commit | 01c3d342f6980a69feb8df27a3454fd721d6298e (patch) | |
| tree | 72f7758dfb38623b1f4a3b6d5cf9c9d98e9bc601 /DomainReportExporter.swift | |
| parent | 54c151e22f8a5d4d7a9c290e79fc7626415f7083 (diff) | |
| download | domain-dig-01c3d342f6980a69feb8df27a3454fd721d6298e.tar.gz domain-dig-01c3d342f6980a69feb8df27a3454fd721d6298e.tar.bz2 domain-dig-01c3d342f6980a69feb8df27a3454fd721d6298e.zip | |
v4.8.0: Add markdown and PDF export formats
- DomainExportFormat gains .markdown and .pdf (CaseIterable, Identifiable,
titled), alongside the existing text/csv/json.
- Markdown reuses the existing text-export content verbatim via a line-based
transform (section "Title\n----" underlines become "## Title", the leading
title becomes an H1, other lines become bullets), so the two formats can
never drift apart.
- PDF renders that Markdown as a simple monospaced multi-page document via
UIGraphicsPDFRenderer (mirrors AuditExporter's existing PDF approach;
degrades to raw Markdown bytes on non-UIKit platforms).
- Replaced the single/batch/tracked-domains/workflow share call sites' hand-
written per-format switches with format-agnostic functions
(exportSingleReportData, exportBatchReportData, exportTrackedDomainsData,
exportWorkflowData) that delegate straight to DomainReportExporter, removing
the now-orphaned per-format helper functions those switches used to call.
Diffstat (limited to 'DomainReportExporter.swift')
| -rw-r--r-- | DomainReportExporter.swift | 102 |
1 files changed, 101 insertions, 1 deletions
diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift index 05ecff9..1a04c70 100644 --- a/DomainReportExporter.swift +++ b/DomainReportExporter.swift @@ -1,11 +1,28 @@ import Foundation -enum DomainExportFormat: String { +#if canImport(UIKit) +import UIKit +#endif + +enum DomainExportFormat: String, CaseIterable, Identifiable { case text = "txt" case csv = "csv" case json = "json" + case markdown = "md" + case pdf = "pdf" + var id: String { rawValue } var fileExtension: String { rawValue } + + var title: String { + switch self { + case .text: return "TXT" + case .csv: return "CSV" + case .json: return "JSON" + case .markdown: return "Markdown" + case .pdf: return "PDF" + } + } } enum DomainReportExporter { @@ -17,6 +34,10 @@ enum DomainReportExporter { return Data(csv(for: [report]).utf8) case .json: return try jsonEncoder.encode(report) + case .markdown: + return Data(markdown(for: report).utf8) + case .pdf: + return pdfData(fromMarkdown: markdown(for: report)) } } @@ -28,7 +49,86 @@ enum DomainReportExporter { return Data(csv(for: reports).utf8) case .json: return try jsonEncoder.encode(reports) + case .markdown: + return Data(batchMarkdown(for: reports, title: title).utf8) + case .pdf: + return pdfData(fromMarkdown: batchMarkdown(for: reports, title: title)) + } + } + + /// Renders `text(for:)`'s content as Markdown: the leading title becomes an + /// H1, `appendSection`'s "Title\n----" underlines become H2 headers, and + /// other non-empty lines that aren't already list items become bullets. + /// This reuses the exact same section content as the text export rather + /// than re-deriving it, so the two formats never drift. + static func markdown(for report: DomainReport) -> String { + markdown(fromPlainText: text(for: report), title: "DomainDig Report") + } + + static func batchMarkdown(for reports: [DomainReport], title: String) -> String { + markdown(fromPlainText: batchText(for: reports, title: title), title: title) + } + + private static func markdown(fromPlainText text: String, title: String) -> String { + let lines = text.components(separatedBy: "\n") + var output: [String] = ["# \(title)", ""] + var index = 0 + while index < lines.count { + let line = lines[index] + if index == 0, line == title { + index += 1 + continue + } + if index + 1 < lines.count, !line.isEmpty, lines[index + 1] == String(repeating: "-", count: line.count) { + output.append("") + output.append("## \(line)") + index += 2 + continue + } + if line.isEmpty || line.hasPrefix("-") || line.hasPrefix(" ") { + output.append(line) + } else { + output.append("- \(line)") + } + index += 1 + } + return output.joined(separator: "\n") + } + + /// Renders Markdown as a simple monospaced multi-page PDF. Foundation-only + /// consumers (no UIKit available) get the Markdown bytes back instead. + static func pdfData(fromMarkdown markdown: String) -> Data { + #if canImport(UIKit) + let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 612, height: 792)) + return renderer.pdfData { context in + let lines = markdown.components(separatedBy: .newlines) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineBreakMode = .byWordWrapping + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.monospacedSystemFont(ofSize: 11, weight: .regular), + .paragraphStyle: paragraphStyle + ] + + var yOffset: CGFloat = 36 + context.beginPage() + + for line in lines { + if yOffset > 744 { + context.beginPage() + yOffset = 36 + } + + let renderedLine = NSString(string: line.isEmpty ? " " : line) + renderedLine.draw( + in: CGRect(x: 36, y: yOffset, width: 540, height: 22), + withAttributes: attributes + ) + yOffset += 16 + } } + #else + return Data(markdown.utf8) + #endif } static func text(for report: DomainReport) -> String { |
