Commit 1c3d19b233

1c3d19b233f135a059f5d111dd1a7514af25ed3f

parent: 91041af60b

Unsigned

cmc <hello@cleberg.net> · 2026-07-20 19:48 UTC

fix: markdown report underlines and duplicate DNS record values

The plain-text-to-Markdown transform only recognized '-' underlines, but
batchText writes '=' — under the document title and as a 48-character
separator between reports. Both leaked through as literal bullets, so a
generated report opened with:

  # Scheduled Watchlist Report
  - ==========================

Title underlines are now consumed alongside the title, '=' underlines
promote to H2 the same as '-', and standalone divider runs render as a
Markdown horizontal rule.

Separately, the DNS section concatenated apex and wildcard records
without dedup. On a domain with wildcard DNS resolving to the same
addresses as the apex, every value was listed twice. Now deduped while
preserving order, so a wildcard value that genuinely differs is still
shown.

Layout: unified · split

DomainReportExporter.swift +29 −2
@@ -77,14 +77,23 @@ enum DomainReportExporter {
7777 let line = lines[index]
7878 if index == 0, line == title {
7979 index += 1
80 if index < lines.count, isUnderline(lines[index], for: line) {
81 index += 1
82 }
8083 continue
8184 }
82 if index + 1 < lines.count, !line.isEmpty, lines[index + 1] == String(repeating: "-", count: line.count) {
85 if index + 1 < lines.count, !line.isEmpty, isUnderline(lines[index + 1], for: line) {
8386 output.append("")
8487 output.append("## \(line)")
8588 index += 2
8689 continue
8790 }
91 if isRule(line) {
92 output.append("")
93 output.append("---")
94 index += 1
95 continue
96 }
8897 if line.isEmpty || line.hasPrefix("-") || line.hasPrefix(" ") {
8998 output.append(line)
9099 } else {
@@ -95,6 +104,21 @@ enum DomainReportExporter {
95104 return output.joined(separator: "\n")
96105 }
97106
107 /// True when `line` is a run of `-` or `=` exactly as long as the heading it
108 /// underlines. `batchText` uses `=` for the document title, `appendSection`
109 /// uses `-` for section headers.
110 private static func isUnderline(_ line: String, for heading: String) -> Bool {
111 guard !heading.isEmpty else { return false }
112 return line == String(repeating: "-", count: heading.count)
113 || line == String(repeating: "=", count: heading.count)
114 }
115
116 /// True for a standalone divider not attached to a heading — `batchText`
117 /// emits a fixed 48-character `=` run between reports.
118 private static func isRule(_ line: String) -> Bool {
119 line.count >= 3 && (line.allSatisfy { $0 == "=" } || line.allSatisfy { $0 == "-" })
120 }
121
98122 /// Renders Markdown as a simple monospaced multi-page PDF. Foundation-only
99123 /// consumers (no UIKit available) get the Markdown bytes back instead.
100124 static func pdfData(fromMarkdown markdown: String) -> Data {
@@ -249,7 +273,10 @@ enum DomainReportExporter {
249273 } else {
250274 dnsLines.append("Records:")
251275 for section in report.dns.recordSections {
252 let values = (section.records + section.wildcardRecords).map(\.value)
276 var seen: Set<String> = []
277 let values = (section.records + section.wildcardRecords)
278 .map(\.value)
279 .filter { seen.insert($0).inserted }
253280 let renderedValues = values.isEmpty ? "None" : values.joined(separator: " | ")
254281 dnsLines.append(" \(section.recordType.rawValue): \(renderedValues)")
255282 }