From 85bad35788f07691b189a9e24e722daafd9cffd8 Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Thu, 30 Oct 2025 21:48:28 +0100 Subject: [PATCH] Refactor release notes to use MarkdownUI library - Create MarkdownContentView to encapsulate MarkdownUI rendering - Replace custom AttributedString markdown parsing with MarkdownUI - Simplify ReleaseNotesView by removing manual markdown styling - Improve markdown rendering with proper support for lists, links, and formatting - Make markdown rendering easily replaceable by keeping it in a dedicated view --- readeck/UI/Settings/MarkdownContentView.swift | 35 ++++++++++++ readeck/UI/Settings/ReleaseNotesView.swift | 55 ++----------------- 2 files changed, 41 insertions(+), 49 deletions(-) create mode 100644 readeck/UI/Settings/MarkdownContentView.swift diff --git a/readeck/UI/Settings/MarkdownContentView.swift b/readeck/UI/Settings/MarkdownContentView.swift new file mode 100644 index 0000000..d105eb4 --- /dev/null +++ b/readeck/UI/Settings/MarkdownContentView.swift @@ -0,0 +1,35 @@ +import SwiftUI +import MarkdownUI + +/// A custom view that renders Markdown content using the MarkdownUI library. +/// This view encapsulates the Markdown rendering logic, making it easy to swap +/// the underlying Markdown library if needed in the future. +struct MarkdownContentView: View { + let content: String + + var body: some View { + Markdown(content) + .textSelection(.enabled) + } +} + +#Preview { + ScrollView { + MarkdownContentView(content: """ +# Heading 1 + +This is a paragraph with **bold** and *italic* text. + +## Heading 2 + +- List item 1 +- List item 2 +- List item 3 + +### Heading 3 + +Another paragraph with [a link](https://example.com). +""") + .padding() + } +} diff --git a/readeck/UI/Settings/ReleaseNotesView.swift b/readeck/UI/Settings/ReleaseNotesView.swift index 76724af..d91999b 100644 --- a/readeck/UI/Settings/ReleaseNotesView.swift +++ b/readeck/UI/Settings/ReleaseNotesView.swift @@ -1,56 +1,14 @@ import SwiftUI -extension AttributedString { - init(styledMarkdown markdownString: String) throws { - var output = try AttributedString( - markdown: markdownString, - options: .init( - allowsExtendedAttributes: true, - interpretedSyntax: .full, - failurePolicy: .returnPartiallyParsedIfPossible - ), - baseURL: nil - ) - - for (intentBlock, intentRange) in output.runs[AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self].reversed() { - guard let intentBlock = intentBlock else { continue } - for intent in intentBlock.components { - switch intent.kind { - case .header(level: let level): - switch level { - case 1: - output[intentRange].font = .system(.title).bold() - case 2: - output[intentRange].font = .system(.title2).bold() - case 3: - output[intentRange].font = .system(.title3).bold() - default: - break - } - default: - break - } - } - - if intentRange.lowerBound != output.startIndex { - output.characters.insert(contentsOf: "\n\n", at: intentRange.lowerBound) - } - } - - self = output - } -} - struct ReleaseNotesView: View { @Environment(\.dismiss) private var dismiss var body: some View { NavigationView { ScrollView { - VStack(alignment: .leading, spacing: 16) { - if let attributedString = loadReleaseNotes() { - Text(attributedString) - .textSelection(.enabled) + VStack(alignment: .leading, spacing: 0) { + if let markdownContent = loadReleaseNotes() { + MarkdownContentView(content: markdownContent) .padding() } else { Text("Unable to load release notes") @@ -71,13 +29,12 @@ struct ReleaseNotesView: View { } } - private func loadReleaseNotes() -> AttributedString? { + private func loadReleaseNotes() -> String? { guard let url = Bundle.main.url(forResource: "RELEASE_NOTES", withExtension: "md"), - let markdownContent = try? String(contentsOf: url), - let attributedString = try? AttributedString(styledMarkdown: markdownContent) else { + let markdownContent = try? String(contentsOf: url) else { return nil } - return attributedString + return markdownContent } }