krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

1c3d19b233f135a059f5d111dd1a7514af25ed3f

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-07-20T19:48:00Z

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.
 DomainReportExporter.swift | 31 +++++++++++++++++++++++++++++--
 1 file changed, 29 insertions(+), 2 deletions(-)

diff --git a/DomainReportExporter.swift b/DomainReportExporter.swift
index 7199d03..a8e2ee9 100644
--- a/DomainReportExporter.swift
+++ b/DomainReportExporter.swift
@@ -77,14 +77,23 @@ enum DomainReportExporter {
             let line = lines[index]
             if index == 0, line == title {
                 index += 1
+                if index < lines.count, isUnderline(lines[index], for: line) {
+                    index += 1
+                }
                 continue
             }
-            if index + 1 < lines.count, !line.isEmpty, lines[index + 1] == String(repeating: "-", count: line.count) {
+            if index + 1 < lines.count, !line.isEmpty, isUnderline(lines[index + 1], for: line) {
                 output.append("")
                 output.append("## \(line)")
                 index += 2
                 continue
             }
+            if isRule(line) {
+                output.append("")
+                output.append("---")
+                index += 1
+                continue
+            }
             if line.isEmpty || line.hasPrefix("-") || line.hasPrefix("  ") {
                 output.append(line)
             } else {
@@ -95,6 +104,21 @@ enum DomainReportExporter {
         return output.joined(separator: "\n")
     }
 
+    /// True when `line` is a run of `-` or `=` exactly as long as the heading it
+    /// underlines. `batchText` uses `=` for the document title, `appendSection`
+    /// uses `-` for section headers.
+    private static func isUnderline(_ line: String, for heading: String) -> Bool {
+        guard !heading.isEmpty else { return false }
+        return line == String(repeating: "-", count: heading.count)
+            || line == String(repeating: "=", count: heading.count)
+    }
+
+    /// True for a standalone divider not attached to a heading — `batchText`
+    /// emits a fixed 48-character `=` run between reports.
+    private static func isRule(_ line: String) -> Bool {
+        line.count >= 3 && (line.allSatisfy { $0 == "=" } || line.allSatisfy { $0 == "-" })
+    }
+
     /// 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 {
@@ -249,7 +273,10 @@ enum DomainReportExporter {
             } else {
                 dnsLines.append("Records:")
                 for section in report.dns.recordSections {
-                    let values = (section.records + section.wildcardRecords).map(\.value)
+                    var seen: Set<String> = []
+                    let values = (section.records + section.wildcardRecords)
+                        .map(\.value)
+                        .filter { seen.insert($0).inserted }
                     let renderedValues = values.isEmpty ? "None" : values.joined(separator: " | ")
                     dnsLines.append("  \(section.recordType.rawValue): \(renderedValues)")
                 }