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
This commit is contained in:
@@ -96,7 +96,6 @@
|
|||||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
||||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA328296DEE006E126D /* SearchModel.swift */; };
|
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA328296DEE006E126D /* SearchModel.swift */; };
|
||||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA728297703006E126D /* InsertSort.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 */; };
|
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3A1D3629637E0500558C0F /* PreviewCache.swift */; };
|
||||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; };
|
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; };
|
||||||
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79C2833036D00E1F516 /* FollowingView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
4C3AC79C2833036D00E1F516 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1245,7 +1243,6 @@
|
|||||||
4CE879492995B58700F758CC /* Relays */,
|
4CE879492995B58700F758CC /* Relays */,
|
||||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
||||||
4C3A1D322960DB0500558C0F /* Markdown.swift */,
|
|
||||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
|
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */,
|
||||||
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
|
||||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
|
||||||
@@ -2092,7 +2089,6 @@
|
|||||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
|
||||||
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
|
||||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
|
||||||
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
|
|
||||||
4C9147002A2A891E00DDEA40 /* error.c in Sources */,
|
4C9147002A2A891E00DDEA40 /* error.c in Sources */,
|
||||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||||
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
|
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
|
||||||
|
|||||||
@@ -1243,7 +1243,7 @@ func render_notification_content_preview(cache: EventCache, ev: NostrEvent, prof
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch artifacts {
|
switch artifacts {
|
||||||
case .parts:
|
case .longform:
|
||||||
// we should never hit this until we have more note types built out of parts
|
// 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
|
// since we handle this case above in known_kind == .longform
|
||||||
return String(ev.content.prefix(prefix_len))
|
return String(ev.content.prefix(prefix_len))
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
import MarkdownUI
|
||||||
|
|
||||||
let eula = """
|
let eula = """
|
||||||
**End User License Agreement**
|
**End User License Agreement**
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ struct EULAView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Text(Markdown.parse(content: eula))
|
Markdown(eula)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 20, leading: 10, bottom: 50, trailing: 10))
|
.padding(EdgeInsets(top: 20, leading: 10, bottom: 50, trailing: 10))
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ struct LongformPreviewBody: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
if case .loaded(let arts) = artifacts.state,
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ struct FollowUserView: View {
|
|||||||
let target: FollowTarget
|
let target: FollowTarget
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
static let markdown = Markdown()
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
|
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import NaturalLanguage
|
import NaturalLanguage
|
||||||
|
import MarkdownUI
|
||||||
|
|
||||||
struct Blur: UIViewRepresentable {
|
struct Blur: UIViewRepresentable {
|
||||||
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
var style: UIBlurEffect.Style = .systemUltraThinMaterial
|
||||||
@@ -212,8 +213,9 @@ struct NoteContentView: View {
|
|||||||
var ArtifactContent: some View {
|
var ArtifactContent: some View {
|
||||||
Group {
|
Group {
|
||||||
switch self.note_artifacts {
|
switch self.note_artifacts {
|
||||||
case .parts(let parts):
|
case .longform(let md):
|
||||||
artifactPartsView(parts.parts)
|
Markdown(md.markdown)
|
||||||
|
.padding(.horizontal)
|
||||||
case .separated(let separated):
|
case .separated(let separated):
|
||||||
MainContent(artifacts: 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 {
|
enum NoteArtifacts {
|
||||||
case separated(NoteArtifactsSeparated)
|
case separated(NoteArtifactsSeparated)
|
||||||
case parts(NoteArtifactsParts)
|
case longform(LongformContent)
|
||||||
|
|
||||||
var images: [URL] {
|
var images: [URL] {
|
||||||
switch self {
|
switch self {
|
||||||
case .separated(let arts):
|
case .separated(let arts):
|
||||||
return arts.images
|
return arts.images
|
||||||
case .parts(let parts):
|
case .longform:
|
||||||
return parts.parts.reduce(into: [URL]()) { acc, part in
|
return []
|
||||||
guard case .media(let m) = part,
|
|
||||||
case .image(let url) = m
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
acc.append(url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +397,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
|||||||
let blocks = ev.blocks(privkey)
|
let blocks = ev.blocks(privkey)
|
||||||
|
|
||||||
if ev.known_kind == .longform {
|
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))
|
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)
|
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 {
|
func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> CompatibleText {
|
||||||
var trimmed = txt
|
var trimmed = txt
|
||||||
|
|
||||||
|
|||||||
@@ -103,8 +103,6 @@ struct ProfileView: View {
|
|||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let pfp_size: CGFloat = 90.0
|
let pfp_size: CGFloat = 90.0
|
||||||
let bannerHeight: CGFloat = 150.0
|
let bannerHeight: CGFloat = 150.0
|
||||||
|
|
||||||
static let markdown = Markdown()
|
|
||||||
|
|
||||||
@State var is_zoomed: Bool = false
|
@State var is_zoomed: Bool = false
|
||||||
@State var show_share_sheet: Bool = false
|
@State var show_share_sheet: Bool = false
|
||||||
|
|||||||
Reference in New Issue
Block a user