damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit bd4c29604fed4eab611fd045baa791d464561262
parent bf1175f22c5ca3ae8a7ffd72a6727887bd769bff
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 16 Jul 2023 14:32:24 -0700

Fix broken markdown renderer

This switches away from the old markdown renderer to the new one at
https://github.com/damus-io/swift-markdown-ui

Changelog-Fixed: Fix broken markdown renderer

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4----
Mdamus/Models/HomeModel.swift | 2+-
Ddamus/Util/Markdown.swift | 88-------------------------------------------------------------------------------
Mdamus/Views/EULAView.swift | 4+++-
Mdamus/Views/Events/Longform/LongformPreview.swift | 4++--
Mdamus/Views/FollowingView.swift | 2--
Mdamus/Views/NoteContentView.swift | 95+++++++++++++++++--------------------------------------------------------------
Mdamus/Views/Profile/ProfileView.swift | 2--
8 files changed, 26 insertions(+), 175 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -96,7 +96,6 @@ 4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; }; 4C363AA428296DEE006E126D /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA328296DEE006E126D /* SearchModel.swift */; }; 4C363AA828297703006E126D /* InsertSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA728297703006E126D /* InsertSort.swift */; }; - 4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3A1D322960DB0500558C0F /* Markdown.swift */; }; 4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3A1D3629637E0500558C0F /* PreviewCache.swift */; }; 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; }; 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79C2833036D00E1F516 /* FollowingView.swift */; }; @@ -540,7 +539,6 @@ 4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; }; 4C363AA328296DEE006E126D /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; }; 4C363AA728297703006E126D /* InsertSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertSort.swift; sourceTree = "<group>"; }; - 4C3A1D322960DB0500558C0F /* Markdown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = "<group>"; }; 4C3A1D3629637E0500558C0F /* PreviewCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCache.swift; sourceTree = "<group>"; }; 4C3AC79A28306D7B00E1F516 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; }; 4C3AC79C2833036D00E1F516 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; }; @@ -1245,7 +1243,6 @@ 4CE879492995B58700F758CC /* Relays */, 4CF0ABEA29844B2F00D66079 /* AnyCodable */, 4CC7AAE6297EFA7B00430951 /* Zap.swift */, - 4C3A1D322960DB0500558C0F /* Markdown.swift */, F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */, 4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */, 4CE4F8CC281352B30009DFBB /* Notifications.swift */, @@ -2092,7 +2089,6 @@ 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, - 4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */, 4C9147002A2A891E00DDEA40 /* error.c in Sources */, 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, 501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */, diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -1243,7 +1243,7 @@ func render_notification_content_preview(cache: EventCache, ev: NostrEvent, prof } switch artifacts { - case .parts: + case .longform: // we should never hit this until we have more note types built out of parts // since we handle this case above in known_kind == .longform return String(ev.content.prefix(prefix_len)) diff --git a/damus/Util/Markdown.swift b/damus/Util/Markdown.swift @@ -1,88 +0,0 @@ -// -// Markdown.swift -// damus -// -// Created by Lionello Lunesu on 2022-12-28. -// - -import Foundation -import SwiftUI - -func count_leading_hashes(_ str: String) -> Int { - var count = 0 - for c in str { - if c == "#" { - count += 1 - } else { - break - } - } - - return count -} - -func get_heading_title_size(count: Int) -> SwiftUI.Font { - if count >= 3 { - return Font.title3 - } else if count >= 2 { - return Font.title2 - } else if count >= 1 { - return Font.title - } - - return Font.body -} - -public struct Markdown { - private var detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - - /// Ensure the specified URL has a scheme by prepending "https://" if it's absent. - static func withScheme(_ url: any StringProtocol) -> any StringProtocol { - return url.contains("://") ? url : "https://" + url - } - - /// Parse a string with markdown into an `AttributedString`, if possible, or else return it as regular text. - public static func parse(content: String) -> AttributedString { - let md_opts: AttributedString.MarkdownParsingOptions = - .init(interpretedSyntax: .inlineOnlyPreservingWhitespace) - - guard content.utf8.count > 0 else { - return AttributedString(stringLiteral: "") - } - - let leading_hashes = count_leading_hashes(content) - if leading_hashes > 0 { - if var str = try? AttributedString(markdown: content) { - str.font = get_heading_title_size(count: leading_hashes) - return str - } - } - - // TODO: escape unintentional markdown - let escaped = content.replacingOccurrences(of: "\\_", with: "\\\\\\_") - if let txt = try? AttributedString(markdown: escaped, options: md_opts) { - return txt - } else { - return AttributedString(stringLiteral: content) - } - } - - /// Process the input text and add markdown for any embedded URLs. - public func process(_ input: String) -> AttributedString { - guard let detector else { - return AttributedString(stringLiteral: input) - } - let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) - var output = input - // Start with the last match, because replacing the first would invalidate all subsequent indices - for match in matches.reversed() { - guard let range = Range(match.range, in: input) - , let url = match.url else { continue } - let text = input[range] - // Use the absoluteString from the matched URL, except when it defaults to http (since we default to https) - let uri = url.scheme == "http" ? Markdown.withScheme(text) : url.absoluteString - output.replaceSubrange(range, with: "[\(text)](\(uri))") - } - return Markdown.parse(content: output) - } -} diff --git a/damus/Views/EULAView.swift b/damus/Views/EULAView.swift @@ -7,6 +7,8 @@ import SwiftUI +import MarkdownUI + let eula = """ **End User License Agreement** @@ -63,7 +65,7 @@ struct EULAView: View { var body: some View { ZStack { ScrollView { - Text(Markdown.parse(content: eula)) + Markdown(eula) .padding() } .padding(EdgeInsets(top: 20, leading: 10, bottom: 50, trailing: 10)) diff --git a/damus/Views/Events/Longform/LongformPreview.swift b/damus/Views/Events/Longform/LongformPreview.swift @@ -58,9 +58,9 @@ struct LongformPreviewBody: View { .foregroundColor(.gray) if case .loaded(let arts) = artifacts.state, - case .parts(let parts) = arts + case .longform(let longform) = arts { - Words(parts.words).font(.footnote) + Words(longform.words).font(.footnote) } } } diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift @@ -11,8 +11,6 @@ struct FollowUserView: View { let target: FollowTarget let damus_state: DamusState - static let markdown = Markdown() - var body: some View { HStack { UserViewRow(damus_state: damus_state, pubkey: target.pubkey) diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -8,6 +8,7 @@ import SwiftUI import LinkPresentation import NaturalLanguage +import MarkdownUI struct Blur: UIViewRepresentable { var style: UIBlurEffect.Style = .systemUltraThinMaterial @@ -212,8 +213,9 @@ struct NoteContentView: View { var ArtifactContent: some View { Group { switch self.note_artifacts { - case .parts(let parts): - artifactPartsView(parts.parts) + case .longform(let md): + Markdown(md.markdown) + .padding(.horizontal) case .separated(let separated): MainContent(artifacts: separated) } @@ -285,22 +287,27 @@ func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText { } } +struct LongformContent { + let markdown: MarkdownContent + let words: Int + + init(_ markdown: String) { + let blocks = [BlockNode].init(markdown: markdown) + self.markdown = MarkdownContent(blocks: blocks) + self.words = count_markdown_words(blocks: blocks) + } +} + enum NoteArtifacts { case separated(NoteArtifactsSeparated) - case parts(NoteArtifactsParts) - + case longform(LongformContent) + var images: [URL] { switch self { case .separated(let arts): return arts.images - case .parts(let parts): - return parts.parts.reduce(into: [URL]()) { acc, part in - guard case .media(let m) = part, - case .image(let url) = m - else { return } - - acc.append(url) - } + case .longform: + return [] } } } @@ -390,7 +397,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) - let blocks = ev.blocks(privkey) if ev.known_kind == .longform { - return .parts(render_blocks_parted(blocks: blocks, profiles: profiles)) + return .longform(LongformContent(ev.content)) } return .separated(render_blocks(blocks: blocks, profiles: profiles)) @@ -409,68 +416,6 @@ fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Tex return (ind, txt) } -func render_blocks_parted(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsParts { - let blocks = bs.blocks - - let new_parts = NoteArtifactsParts(parts: [], words: bs.words) - - return blocks.reduce(into: new_parts) { parts, block in - - switch block { - case .mention(let m): - guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { - parts.parts.append(.text(mention_str(m, profiles: profiles).text)) - return - } - parts.parts[last_ind] = .text(txt + mention_str(m, profiles: profiles).text) - - case .text(let str): - guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { - // TODO: (jb55) md is longform specific - let md = Markdown.parse(content: str) - parts.parts.append(.text(Text(md))) - return - } - - parts.parts[last_ind] = .text(txt + Text(str)) - - case .relay(let relay): - guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { - parts.parts.append(.text(Text(relay))) - return - } - - parts.parts[last_ind] = .text(txt + Text(relay)) - - case .hashtag(let htag): - guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { - parts.parts.append(.text(hashtag_str(htag).text)) - return - } - - parts.parts[last_ind] = .text(txt + hashtag_str(htag).text) - - case .invoice(let invoice): - parts.parts.append(.invoice(invoice)) - return - - case .url(let url): - let url_type = classify_url(url) - switch url_type { - case .media(let media_url): - parts.parts.append(.media(media_url)) - case .link(let url): - guard let (last_ind, txt) = artifact_part_last_text_ind(parts: parts.parts) else { - parts.parts.append(.text(url_str(url).text)) - return - } - - parts.parts[last_ind] = .text(txt + url_str(url).text) - } - } - } -} - func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> CompatibleText { var trimmed = txt diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -103,8 +103,6 @@ struct ProfileView: View { let damus_state: DamusState let pfp_size: CGFloat = 90.0 let bannerHeight: CGFloat = 150.0 - - static let markdown = Markdown() @State var is_zoomed: Bool = false @State var show_share_sheet: Bool = false