From 43382b23be4fc7330f4af72f4ac4f1c89cd77f18 Mon Sep 17 00:00:00 2001 From: LiYanan2004 <37542129+LiYanan2004@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:50:42 +0800 Subject: [PATCH 1/4] Updated for swift 6 and complete concurrency checking --- Package.resolved | 11 +++---- Package.swift | 7 +++-- .../BlockDirectiveModifiers.swift | 6 ++-- .../BlockDirectiveRenderer.swift | 8 +++-- .../Customization/CodeHighlighter.swift | 4 +-- .../Font/AnyMarkdownFontGroup.swift | 2 +- .../Customization/Font/DefaultFontGroup.swift | 2 +- .../Customization/Font/FontModifiers.swift | 2 +- .../AnyForegroundStyleGroup.swift | 2 +- .../ForegroundStyleModifiers.swift | 2 +- .../Customization/LayoutRole.swift | 4 +-- Sources/MarkdownView/Customization/List.swift | 6 ++-- .../MarkdownView/Customization/Spacing.swift | 2 +- .../Customization/TintColor.swift | 4 +-- .../MarkdownView/Helper/ContainerSize.swift | 2 +- .../MarkdownView/Helper/ContentUpdater.swift | 12 ++++---- .../MarkdownView/Helper/ScrollProxyRef.swift | 7 ++++- Sources/MarkdownView/Helper/ViewContent.swift | 5 ++-- .../MarkdownView/Image/ImageDisplayable.swift | 16 +++++++--- .../Image/ImageProviderModifiers.swift | 4 +-- .../MarkdownView/Image/ImageRenderer.swift | 18 +++++++---- Sources/MarkdownView/MarkdownView.swift | 30 ++++++++++--------- .../Renderer/Renderer+BlockDirective.swift | 2 +- .../MarkdownView/Renderer/Renderer+List.swift | 2 ++ .../Renderer/Renderer+Table.swift | 2 ++ Sources/MarkdownView/Renderer/Renderer.swift | 4 +-- Sources/MarkdownView/SendableMarkdown.swift | 8 +++++ Sources/MarkdownView/View/CodeBlockView.swift | 23 ++++++++++---- Sources/MarkdownView/View/SVGView.swift | 28 +++++++++-------- 29 files changed, 140 insertions(+), 85 deletions(-) create mode 100644 Sources/MarkdownView/SendableMarkdown.swift diff --git a/Package.resolved b/Package.resolved index b01f95eb..e1220fc7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "a316d3d5c0488b4a2e2430d33fa1c40cede85e70129a4d26ccb106364c4ce0f9", "pins" : [ { "identity" : "highlightr", @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-cmark.git", "state" : { - "revision" : "29d9c97e6310b87c4799268eaa2fc76164b2dbd8", - "version" : "0.2.0" + "revision" : "3bc2f3e25df0cecc5dc269f7ccae65d0f386f06a", + "version" : "0.4.0" } }, { @@ -23,10 +24,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-markdown.git", "state" : { - "revision" : "68b2fed9fb12fb71ac81e537f08bed430b189e35", - "version" : "0.2.0" + "revision" : "4aae40bf6fff5286e0e1672329d17824ce16e081", + "version" : "0.4.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index fcaa18ca..a1b4fdd6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -15,7 +15,7 @@ let package = Package( .library(name: "MarkdownView", targets: ["MarkdownView"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-markdown.git", from: "0.2.0"), + .package(url: "https://github.com/apple/swift-markdown.git", from: "0.4.0"), .package(url: "https://github.com/raspu/Highlightr.git", from: "2.1.2"), ], targets: [ @@ -28,7 +28,8 @@ let package = Package( package: "Highlightr", condition: .when(platforms: [.iOS, .macOS]) ), - ] + ], + swiftSettings: [.swiftLanguageVersion(.v6)] ), ] ) diff --git a/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveModifiers.swift b/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveModifiers.swift index 49ab99fe..ec52f100 100644 --- a/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveModifiers.swift +++ b/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveModifiers.swift @@ -21,9 +21,9 @@ extension View { } // MARK: - Environment Values - -struct MarkdownBlockDirectiveKey: EnvironmentKey { - static var defaultValue = BlockDirectiveRenderer() +@MainActor +struct MarkdownBlockDirectiveKey: @preconcurrency EnvironmentKey { + static let defaultValue = BlockDirectiveRenderer() } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveRenderer.swift b/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveRenderer.swift index da6e1466..c2559e53 100644 --- a/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveRenderer.swift +++ b/Sources/MarkdownView/Customization/BlockDirective/BlockDirectiveRenderer.swift @@ -1,10 +1,10 @@ import SwiftUI import Markdown -class BlockDirectiveRenderer { +class BlockDirectiveRenderer: @unchecked Sendable { /// All providers which have been added. var providers: [String : any BlockDirectiveDisplayable] = [:] - + /// Add custom provider for block directive . /// - Parameters: /// - provider: Represention of the block directive. @@ -19,7 +19,9 @@ class BlockDirectiveRenderer { text: String ) -> AnyView? { if let provider { - return AnyView(provider.makeView(arguments: args, text: text)) + return provider.makeView( + arguments: args, text: text + ).eraseToAnyView() } return nil } diff --git a/Sources/MarkdownView/Customization/CodeHighlighter.swift b/Sources/MarkdownView/Customization/CodeHighlighter.swift index 9c9c1f55..84ce1fbb 100644 --- a/Sources/MarkdownView/Customization/CodeHighlighter.swift +++ b/Sources/MarkdownView/Customization/CodeHighlighter.swift @@ -3,7 +3,7 @@ import SwiftUI /// The theme of code highlighter. /// /// - note: For more information, Check out [raspu/Highlightr](https://github.com/raspu/Highlightr) . -public struct CodeHighlighterTheme: Equatable { +public struct CodeHighlighterTheme: Equatable, Sendable { /// The theme name in Light Mode. var lightModeThemeName: String @@ -35,7 +35,7 @@ public struct CodeHighlighterTheme: Equatable { } struct CodeHighlighterThemeKey: EnvironmentKey { - static var defaultValue: CodeHighlighterTheme = CodeHighlighterTheme( + static let defaultValue: CodeHighlighterTheme = CodeHighlighterTheme( lightModeThemeName: "xcode", darkModeThemeName: "dark" ) } diff --git a/Sources/MarkdownView/Customization/Font/AnyMarkdownFontGroup.swift b/Sources/MarkdownView/Customization/Font/AnyMarkdownFontGroup.swift index b7ee0212..7028c425 100644 --- a/Sources/MarkdownView/Customization/Font/AnyMarkdownFontGroup.swift +++ b/Sources/MarkdownView/Customization/Font/AnyMarkdownFontGroup.swift @@ -1,7 +1,7 @@ import SwiftUI /// A type-erased MarkdownFontGroup value. -public struct AnyMarkdownFontGroup { +public struct AnyMarkdownFontGroup: Sendable { var _h1: Font var _h2: Font var _h3: Font diff --git a/Sources/MarkdownView/Customization/Font/DefaultFontGroup.swift b/Sources/MarkdownView/Customization/Font/DefaultFontGroup.swift index a4a210ba..fb6ef37f 100644 --- a/Sources/MarkdownView/Customization/Font/DefaultFontGroup.swift +++ b/Sources/MarkdownView/Customization/Font/DefaultFontGroup.swift @@ -3,7 +3,7 @@ import SwiftUI /// The font group that describes a set of platform’s dynamic types for each component. /// /// Use ``MarkdownView/MarkdownFontGroup/automatic`` to construct this type. -public struct DefaultFontGroup: MarkdownFontGroup { } +public struct DefaultFontGroup: MarkdownFontGroup, Sendable { } extension MarkdownFontGroup where Self == DefaultFontGroup { /// The font group that describes a set of platform’s dynamic types for each component. diff --git a/Sources/MarkdownView/Customization/Font/FontModifiers.swift b/Sources/MarkdownView/Customization/Font/FontModifiers.swift index 64ed40db..694e341d 100644 --- a/Sources/MarkdownView/Customization/Font/FontModifiers.swift +++ b/Sources/MarkdownView/Customization/Font/FontModifiers.swift @@ -48,7 +48,7 @@ public extension View { // MARK: - Environment Values struct MarkdownFontGroupKey: EnvironmentKey { - static var defaultValue = AnyMarkdownFontGroup(.automatic) + static let defaultValue = AnyMarkdownFontGroup(.automatic) } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Customization/ForegroundStyle/AnyForegroundStyleGroup.swift b/Sources/MarkdownView/Customization/ForegroundStyle/AnyForegroundStyleGroup.swift index ce1cd1ed..445821ba 100644 --- a/Sources/MarkdownView/Customization/ForegroundStyle/AnyForegroundStyleGroup.swift +++ b/Sources/MarkdownView/Customization/ForegroundStyle/AnyForegroundStyleGroup.swift @@ -1,7 +1,7 @@ import SwiftUI /// A type-erased MarkdownForegroundStyleGroup value. -public struct AnyMarkdownForegroundStyleGroup { +public struct AnyMarkdownForegroundStyleGroup: Sendable { var _h1: AnyShapeStyle var _h2: AnyShapeStyle var _h3: AnyShapeStyle diff --git a/Sources/MarkdownView/Customization/ForegroundStyle/ForegroundStyleModifiers.swift b/Sources/MarkdownView/Customization/ForegroundStyle/ForegroundStyleModifiers.swift index b0f37bb0..9f829bd7 100644 --- a/Sources/MarkdownView/Customization/ForegroundStyle/ForegroundStyleModifiers.swift +++ b/Sources/MarkdownView/Customization/ForegroundStyle/ForegroundStyleModifiers.swift @@ -42,7 +42,7 @@ public extension View { // MARK: - Environment Values struct MarkdownForegroundStyleGroupKey: EnvironmentKey { - static var defaultValue = AnyMarkdownForegroundStyleGroup(.automatic) + static let defaultValue = AnyMarkdownForegroundStyleGroup(.automatic) } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Customization/LayoutRole.swift b/Sources/MarkdownView/Customization/LayoutRole.swift index 84d78579..f55f638a 100644 --- a/Sources/MarkdownView/Customization/LayoutRole.swift +++ b/Sources/MarkdownView/Customization/LayoutRole.swift @@ -1,7 +1,7 @@ import SwiftUI /// The role of MarkdownView, which affects how MarkdownView is rendered. -public enum MarkdownViewRole { +public enum MarkdownViewRole: Sendable { /// The normal role. /// /// A role that makes the view take the space it needs and center contents, like a normal SwiftUI View. @@ -18,7 +18,7 @@ public enum MarkdownViewRole { } struct MarkdownViewRoleKey: EnvironmentKey { - static var defaultValue = MarkdownViewRole.normal + static let defaultValue = MarkdownViewRole.normal } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Customization/List.swift b/Sources/MarkdownView/Customization/List.swift index 3c620091..d70282fa 100644 --- a/Sources/MarkdownView/Customization/List.swift +++ b/Sources/MarkdownView/Customization/List.swift @@ -1,7 +1,7 @@ import SwiftUI struct ListIndentEnvironmentKey: EnvironmentKey { - static var defaultValue: CGFloat = 12 + static let defaultValue: CGFloat = 12 } extension EnvironmentValues { @@ -19,7 +19,7 @@ public extension View { struct UnorderedListBulletEnvironmentKey: EnvironmentKey { - static var defaultValue: String = "•" + static let defaultValue: String = "•" } extension EnvironmentValues { @@ -37,7 +37,7 @@ public extension View { struct UnorderedListBulletFontEnvironmentKey: EnvironmentKey { - static var defaultValue: Font = .title2.weight(.black) + static let defaultValue: Font = .title2.weight(.black) } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Customization/Spacing.swift b/Sources/MarkdownView/Customization/Spacing.swift index 5e17e178..2b906a70 100644 --- a/Sources/MarkdownView/Customization/Spacing.swift +++ b/Sources/MarkdownView/Customization/Spacing.swift @@ -1,7 +1,7 @@ import SwiftUI struct ComponentSpacingEnvironmentKey: EnvironmentKey { - static var defaultValue: CGFloat = 8 + static let defaultValue: CGFloat = 8 } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Customization/TintColor.swift b/Sources/MarkdownView/Customization/TintColor.swift index 878420bb..35a4b761 100644 --- a/Sources/MarkdownView/Customization/TintColor.swift +++ b/Sources/MarkdownView/Customization/TintColor.swift @@ -1,11 +1,11 @@ import SwiftUI struct BlockQuoteTint: EnvironmentKey { - static var defaultValue: Color = Color.accentColor + static let defaultValue: Color = Color.accentColor } struct InlineCodeBlockTint: EnvironmentKey { - static var defaultValue = Color.accentColor + static let defaultValue = Color.accentColor } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Helper/ContainerSize.swift b/Sources/MarkdownView/Helper/ContainerSize.swift index 1c19d242..88c5afce 100644 --- a/Sources/MarkdownView/Helper/ContainerSize.swift +++ b/Sources/MarkdownView/Helper/ContainerSize.swift @@ -1,7 +1,7 @@ import SwiftUI struct ContainerSize: EnvironmentKey { - static var defaultValue: CGSize = .zero + static let defaultValue: CGSize = .zero } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Helper/ContentUpdater.swift b/Sources/MarkdownView/Helper/ContentUpdater.swift index d37e8ee3..ba433b2f 100644 --- a/Sources/MarkdownView/Helper/ContentUpdater.swift +++ b/Sources/MarkdownView/Helper/ContentUpdater.swift @@ -21,12 +21,14 @@ class ContentUpdater: ObservableObject { } class MarkdownTextStorage: ObservableObject { - static var `default` = MarkdownTextStorage() + @MainActor static let `default` = MarkdownTextStorage() @Published var text: String? = nil + + internal init() { } } /// A Markdown Rendering Mode. -public enum MarkdownRenderingMode { +public enum MarkdownRenderingMode: Sendable { /// Immediately re-render markdown view when text changes. case immediate /// Re-render markdown view efficiently by adding a debounce to the pipeline. @@ -36,11 +38,11 @@ public enum MarkdownRenderingMode { } struct MarkdownRenderingModeKey: EnvironmentKey { - static var defaultValue: MarkdownRenderingMode = .immediate + static let defaultValue: MarkdownRenderingMode = .immediate } /// Thread to render markdown content on. -public enum MarkdownRenderingThread { +public enum MarkdownRenderingThread: Sendable { /// Render & Update markdown content on main thread. case main /// Render markdown content on background thread, while updating view on main thread. @@ -48,7 +50,7 @@ public enum MarkdownRenderingThread { } struct MarkdownRenderingThreadKey: EnvironmentKey { - static var defaultValue: MarkdownRenderingThread = .background + static let defaultValue: MarkdownRenderingThread = .background } extension EnvironmentValues { diff --git a/Sources/MarkdownView/Helper/ScrollProxyRef.swift b/Sources/MarkdownView/Helper/ScrollProxyRef.swift index fa063015..2c2321ed 100644 --- a/Sources/MarkdownView/Helper/ScrollProxyRef.swift +++ b/Sources/MarkdownView/Helper/ScrollProxyRef.swift @@ -1,6 +1,11 @@ import SwiftUI +@MainActor class ScrollProxyRef { static var shared = ScrollProxyRef() - var proxy: ScrollViewProxy? + private(set) var proxy: ScrollViewProxy? + + func setProxy(_ proxy: ScrollViewProxy) { + self.proxy = proxy + } } diff --git a/Sources/MarkdownView/Helper/ViewContent.swift b/Sources/MarkdownView/Helper/ViewContent.swift index 384550f6..7a835c11 100644 --- a/Sources/MarkdownView/Helper/ViewContent.swift +++ b/Sources/MarkdownView/Helper/ViewContent.swift @@ -1,6 +1,6 @@ import SwiftUI -struct ViewContent { +struct ViewContent: @unchecked Sendable { var text: Text var view: AnyView var type: ContentType @@ -30,6 +30,7 @@ struct ViewContent { /// Create a content descriptor. /// - Parameter content: Any view that comforms to View protocol. + init(_ content: some View) { text = Text("") view = AnyView(content) @@ -110,7 +111,7 @@ extension ViewContent { } extension View { - func eraseToAnyView() -> AnyView { + nonisolated func eraseToAnyView() -> AnyView { AnyView(self) } } diff --git a/Sources/MarkdownView/Image/ImageDisplayable.swift b/Sources/MarkdownView/Image/ImageDisplayable.swift index 5b3b436f..be9ad7f2 100644 --- a/Sources/MarkdownView/Image/ImageDisplayable.swift +++ b/Sources/MarkdownView/Image/ImageDisplayable.swift @@ -46,18 +46,26 @@ struct AssetImageDisplayable: ImageDisplayable { nsImage = NSImage(named: name(url)) } if let nsImage { - return AssetImage(image: nsImage, alt: alt) + return MainActor.assumeIsolated { + AssetImage(image: nsImage, alt: alt) + } } #elseif os(iOS) || os(tvOS) if let uiImage = UIImage(named: name(url), in: bundle, compatibleWith: nil) { - return AssetImage(image: uiImage, alt: alt) + return MainActor.assumeIsolated { + AssetImage(image: uiImage, alt: alt) + } } #elseif os(watchOS) if let uiImage = UIImage(named: name(url), in: bundle, with: nil) { - return AssetImage(image: uiImage, alt: alt) + return MainActor.assumeIsolated { + AssetImage(image: uiImage, alt: alt) + } } #endif - return AssetImage(image: nil, alt: nil) + return MainActor.assumeIsolated { + AssetImage(image: nil, alt: nil) + } } } diff --git a/Sources/MarkdownView/Image/ImageProviderModifiers.swift b/Sources/MarkdownView/Image/ImageProviderModifiers.swift index 74dd9ed7..2dc7aee0 100644 --- a/Sources/MarkdownView/Image/ImageProviderModifiers.swift +++ b/Sources/MarkdownView/Image/ImageProviderModifiers.swift @@ -22,8 +22,8 @@ extension View { // MARK: - Environment Values - -struct MarkdownImageRendererKey: EnvironmentKey { +@MainActor +struct MarkdownImageRendererKey: @preconcurrency EnvironmentKey { static var defaultValue = ImageRenderer() } diff --git a/Sources/MarkdownView/Image/ImageRenderer.swift b/Sources/MarkdownView/Image/ImageRenderer.swift index f2e835f8..e0af87e0 100644 --- a/Sources/MarkdownView/Image/ImageRenderer.swift +++ b/Sources/MarkdownView/Image/ImageRenderer.swift @@ -1,8 +1,8 @@ import SwiftUI -class ImageRenderer { +class ImageRenderer: @unchecked Sendable { /// The base URL for local images or network images. - var baseURL: URL + private(set) var baseURL: URL /// Create a Configuration for image handling. init(baseURL: URL? = nil) { @@ -21,7 +21,7 @@ class ImageRenderer { } /// All the providers that have been added. - var imageProviders: [String: any ImageDisplayable] = [ + private(set) var imageProviders: [String: any ImageDisplayable] = [ "http": NetworkImageDisplayable(), "https": NetworkImageDisplayable(), ] @@ -41,11 +41,19 @@ class ImageRenderer { ) -> AnyView { if let provider { // Found a specific provider. - return AnyView(provider.makeImage(url: url, alt: alt)) + provider.makeImage(url: url, alt: alt) + .eraseToAnyView() } else { // No specific provider. // Try to load the image from the Base URL. - return AnyView(RelativePathImageDisplayable(baseURL: baseURL).makeImage(url: url, alt: alt)) + RelativePathImageDisplayable(baseURL: baseURL) + .makeImage(url: url, alt: alt) + .eraseToAnyView() } } + + func updateBaseURL(_ baseURL: URL?) { + guard let baseURL else { return } + self.baseURL = baseURL + } } diff --git a/Sources/MarkdownView/MarkdownView.swift b/Sources/MarkdownView/MarkdownView.swift index 83a4b220..15b1fb07 100644 --- a/Sources/MarkdownView/MarkdownView.swift +++ b/Sources/MarkdownView/MarkdownView.swift @@ -1,6 +1,5 @@ import SwiftUI import Markdown -import Combine /// A view to render markdown text. public struct MarkdownView: View { @@ -55,7 +54,7 @@ public struct MarkdownView: View { public var body: some View { ScrollViewReader { scrollProxy in if renderingThread == .main { - _makeView(text: text) + makeView(text: text) } else { ZStack { switch configuration.role { @@ -65,7 +64,7 @@ public struct MarkdownView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } } - .onAppear { scrollViewRef.proxy = scrollProxy } + .onAppear { scrollViewRef.setProxy(scrollProxy) } } } .sizeOfView($viewSize) @@ -75,34 +74,32 @@ public struct MarkdownView: View { .if(renderingMode == .optimized && renderingThread == .background) { content in content // Received a debouncedText, we need to reload MarkdownView. - .onReceive(contentUpdater.textUpdater, perform: makeView(text:)) + .onReceive(contentUpdater.textUpdater, perform: updateView(text:)) // Push current text, waiting for next update. .onChange(of: text, perform: contentUpdater.push(_:)) } .if(renderingMode == .immediate && renderingThread == .background) { content in content // Immediately update MarkdownView when text changes. - .onChange(of: text, perform: makeView(text:)) + .onChange(of: text, perform: updateView(text:)) } // Load view immediately after the first launch. // Receive configuration changes and reload MarkdownView to fit. - .task(id: configuration) { makeView(text: text) } - .task(id: baseURL) { imageRenderer.baseURL = baseURL ?? imageRenderer.baseURL } - } - - private func makeView(text: String) { - representedView = _makeView(text: text) - MarkdownTextStorage.default.text = text + .task(id: configuration) { updateView(text: text) } + .task(id: baseURL) { + guard let baseURL else { return } + imageRenderer.updateBaseURL(baseURL) + } } - private func _makeView(text: String) -> AnyView { + private func makeView(text: String) -> AnyView { var renderer = Renderer( text: text, configuration: configuration, interactiveEditHandler: { text in Task { @MainActor in self.text = text - self.makeView(text: text) + self.updateView(text: text) } }, blockDirectiveRenderer: blockDirectiveRenderer, @@ -111,6 +108,11 @@ public struct MarkdownView: View { let parseBD = !blockDirectiveRenderer.providers.isEmpty return renderer.representedView(parseBlockDirectives: parseBD) } + + private func updateView(text: String) { + representedView = makeView(text: text) + MarkdownTextStorage.default.text = text + } } extension MarkdownView { diff --git a/Sources/MarkdownView/Renderer/Renderer+BlockDirective.swift b/Sources/MarkdownView/Renderer/Renderer+BlockDirective.swift index 04942b8a..ba74860a 100644 --- a/Sources/MarkdownView/Renderer/Renderer+BlockDirective.swift +++ b/Sources/MarkdownView/Renderer/Renderer+BlockDirective.swift @@ -5,7 +5,7 @@ extension Renderer { mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> Result { var provider: (any BlockDirectiveDisplayable)? blockDirectiveRenderer.providers.forEach { name, value in - if name.lowercased() == blockDirective.name.lowercased() { + if name.localizedLowercase == blockDirective.name.localizedLowercase { provider = value } } diff --git a/Sources/MarkdownView/Renderer/Renderer+List.swift b/Sources/MarkdownView/Renderer/Renderer+List.swift index f965a50f..b18b8915 100644 --- a/Sources/MarkdownView/Renderer/Renderer+List.swift +++ b/Sources/MarkdownView/Renderer/Renderer+List.swift @@ -14,6 +14,7 @@ extension Renderer { } } + @MainActor mutating func visitOrderedList(_ orderedList: OrderedList) -> Result { Result { let listItems = orderedList.children.map { $0 as! ListItem } @@ -39,6 +40,7 @@ extension Renderer { } } + @MainActor mutating func visitUnorderedList(_ unorderedList: UnorderedList) -> Result { Result { let listItems = unorderedList.children.map { $0 as! ListItem } diff --git a/Sources/MarkdownView/Renderer/Renderer+Table.swift b/Sources/MarkdownView/Renderer/Renderer+Table.swift index cf5b4838..7bf8f226 100644 --- a/Sources/MarkdownView/Renderer/Renderer+Table.swift +++ b/Sources/MarkdownView/Renderer/Renderer+Table.swift @@ -2,6 +2,7 @@ import SwiftUI import Markdown extension Renderer { + @MainActor mutating func visitTable(_ table: Markdown.Table) -> Result { if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { return Result { @@ -71,6 +72,7 @@ extension Renderer { } } + @MainActor mutating func visitTableRow(_ row: Markdown.Table.Row) -> Result { Result { let cells = row.children.map { $0 as! Markdown.Table.Cell } diff --git a/Sources/MarkdownView/Renderer/Renderer.swift b/Sources/MarkdownView/Renderer/Renderer.swift index 66d6dd2b..fc040eb7 100644 --- a/Sources/MarkdownView/Renderer/Renderer.swift +++ b/Sources/MarkdownView/Renderer/Renderer.swift @@ -1,7 +1,7 @@ import SwiftUI import Markdown -struct Renderer: MarkupVisitor { +struct Renderer: @preconcurrency MarkupVisitor { typealias Result = ViewContent var text: String @@ -110,7 +110,7 @@ struct Renderer: MarkupVisitor { var provider: (any ImageDisplayable)? if let scheme = source.scheme { imageRenderer.imageProviders.forEach { key, value in - if scheme.lowercased() == key.lowercased() { + if scheme.localizedLowercase == key.localizedLowercase { provider = value return } diff --git a/Sources/MarkdownView/SendableMarkdown.swift b/Sources/MarkdownView/SendableMarkdown.swift new file mode 100644 index 00000000..d5ce03bd --- /dev/null +++ b/Sources/MarkdownView/SendableMarkdown.swift @@ -0,0 +1,8 @@ +import Markdown + +// TODO: Remove these when swift-markdown adapted for swift 6.0 +extension Markdown.Table: @retroactive @unchecked Sendable { } +extension Markdown.Table.Row: @retroactive @unchecked Sendable { } +extension Markdown.OrderedList: @retroactive @unchecked Sendable { } +extension Markdown.UnorderedList: @retroactive @unchecked Sendable { } +extension Markdown.ParseOptions: @retroactive @unchecked Sendable { } diff --git a/Sources/MarkdownView/View/CodeBlockView.swift b/Sources/MarkdownView/View/CodeBlockView.swift index d2859170..5ee37780 100644 --- a/Sources/MarkdownView/View/CodeBlockView.swift +++ b/Sources/MarkdownView/View/CodeBlockView.swift @@ -1,7 +1,7 @@ import SwiftUI #if canImport(Highlightr) -import Highlightr +@preconcurrency import Highlightr #endif #if canImport(Highlightr) @@ -60,12 +60,22 @@ struct HighlightedCodeBlock: View { } } - @Sendable private func highlight() async { + private func highlight() { guard let highlighter = Highlightr.shared else { return } - let language = highlighter.supportedLanguages().first(where: { $0.lowercased() == self.language?.lowercased() }) - if let highlightedCode = highlighter.highlight(code, as: language) { - withAnimation { - attributedCode = AttributedString(highlightedCode) + let language = self.language?.lowercased() + let originalCode = code + + Task.detached { [language, highlighter] in + let language = highlighter.supportedLanguages() + .first(where: { $0.localizedLowercase == language }) + async let highlight = highlighter.highlight(originalCode, as: language) + guard let highlightedCode = await highlight else { return } + let attributedCode = AttributedString(highlightedCode) + + Task { @MainActor in + withAnimation { + self.attributedCode = attributedCode + } } } } @@ -76,6 +86,7 @@ struct HighlightedCodeBlock: View { #if canImport(Highlightr) extension Highlightr { + @MainActor static var shared: Highlightr? = Highlightr() } #endif diff --git a/Sources/MarkdownView/View/SVGView.swift b/Sources/MarkdownView/View/SVGView.swift index 43219486..16b0cf14 100644 --- a/Sources/MarkdownView/View/SVGView.swift +++ b/Sources/MarkdownView/View/SVGView.swift @@ -34,24 +34,26 @@ struct SVGView: View { } // MARK: - WKWebView Delegate - -class Coordinator: NSObject, WKNavigationDelegate { +@MainActor +fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate { var updateSize: ((CGSize) -> Void)? init(updateSize: ((CGSize) -> Void)?) { self.updateSize = updateSize } - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // Get SVG size via DIV container. - webView.evaluateJavaScript("var style = window.getComputedStyle ? window.getComputedStyle(svg_content,null) : null || svg_content.currentStyle;") - webView.evaluateJavaScript("style.width") { width, _ in - guard let width = (width as? String)?.htmlSize() else { return } - self.updateSize?(CGSize(width: width, height: .zero)) - } - webView.evaluateJavaScript("style.height") { height, _ in - guard let height = (height as? String)?.htmlSize() else { return } - self.updateSize?(CGSize(width: .zero, height: height)) + nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + MainActor.assumeIsolated { + // Get SVG size via DIV container. + webView.evaluateJavaScript("var style = window.getComputedStyle ? window.getComputedStyle(svg_content,null) : null || svg_content.currentStyle;") + webView.evaluateJavaScript("style.width") { width, _ in + guard let width = (width as? String)?.htmlSize() else { return } + self.updateSize?(CGSize(width: width, height: .zero)) + } + webView.evaluateJavaScript("style.height") { height, _ in + guard let height = (height as? String)?.htmlSize() else { return } + self.updateSize?(CGSize(width: .zero, height: height)) + } } } } @@ -79,7 +81,7 @@ fileprivate struct _SVGViewBridge: NSViewRepresentable { } } - func makeCoordinator() -> Coordinator { + func makeCoordinator() -> WebViewDelegate { Coordinator(updateSize: updateSize) } } From 8571bb2cca75b763be2f350fc814e6373025c61a Mon Sep 17 00:00:00 2001 From: Yanan Li <1474700628@qq.com> Date: Wed, 23 Oct 2024 14:11:06 +0800 Subject: [PATCH 2/4] Update swift-markdown package version --- Package.resolved | 12 ++++++------ Package.swift | 4 ++-- Package@5.10.swift | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 Package@5.10.swift diff --git a/Package.resolved b/Package.resolved index e1220fc7..e592b257 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a316d3d5c0488b4a2e2430d33fa1c40cede85e70129a4d26ccb106364c4ce0f9", + "originHash" : "4fcc1aeb59dcf0a651cfc2aba3a5253976afe1c80e8d88dd48080568e80ec3fc", "pins" : [ { "identity" : "highlightr", @@ -15,17 +15,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-cmark.git", "state" : { - "revision" : "3bc2f3e25df0cecc5dc269f7ccae65d0f386f06a", - "version" : "0.4.0" + "revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53", + "version" : "0.5.0" } }, { "identity" : "swift-markdown", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-markdown.git", + "location" : "https://github.com/swiftlang/swift-markdown.git", "state" : { - "revision" : "4aae40bf6fff5286e0e1672329d17824ce16e081", - "version" : "0.4.0" + "revision" : "8f79cb175981458a0a27e76cb42fee8e17b1a993", + "version" : "0.5.0" } } ], diff --git a/Package.swift b/Package.swift index a1b4fdd6..727db047 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( .library(name: "MarkdownView", targets: ["MarkdownView"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-markdown.git", from: "0.4.0"), + .package(url: "https://github.com/swiftlang/swift-markdown.git", from: "0.5.0"), .package(url: "https://github.com/raspu/Highlightr.git", from: "2.1.2"), ], targets: [ @@ -29,7 +29,7 @@ let package = Package( condition: .when(platforms: [.iOS, .macOS]) ), ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageMode(.v6)] ), ] ) diff --git a/Package@5.10.swift b/Package@5.10.swift new file mode 100644 index 00000000..717968cf --- /dev/null +++ b/Package@5.10.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MarkdownView", + platforms: [ + .macOS(.v12), + .iOS(.v15), + .tvOS(.v15), + .watchOS(.v8), + ], + products: [ + .library(name: "MarkdownView", targets: ["MarkdownView"]), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-markdown.git", from: "0.5.0"), + .package(url: "https://github.com/raspu/Highlightr.git", from: "2.1.2"), + ], + targets: [ + .target( + name: "MarkdownView", + dependencies: [ + .product(name: "Markdown", package: "swift-markdown"), + .product( + name: "Highlightr", + package: "Highlightr", + condition: .when(platforms: [.iOS, .macOS]) + ), + ] + ), + ] +) From c96f69a86f3c7c8022a06f4c5b4b1316535c8fe3 Mon Sep 17 00:00:00 2001 From: Yanan Li <1474700628@qq.com> Date: Wed, 23 Oct 2024 15:48:38 +0800 Subject: [PATCH 3/4] fix language issue of code highlight --- .../Extensions/AnyShapeStyle+Equatable.swift | 2 +- .../MarkdownView/Helper/ActorIsolated.swift | 9 +++ Sources/MarkdownView/MarkdownView.swift | 2 +- .../CodeHighlighterUpdater.swift | 36 +++++++++++ .../CodeHighlight/Highlightr+Shared.swift | 9 +++ Sources/MarkdownView/View/CodeBlockView.swift | 63 ++++--------------- Sources/MarkdownView/View/SVGView.swift | 5 +- 7 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 Sources/MarkdownView/Helper/ActorIsolated.swift create mode 100644 Sources/MarkdownView/Renderer/CodeHighlight/CodeHighlighterUpdater.swift create mode 100644 Sources/MarkdownView/Renderer/CodeHighlight/Highlightr+Shared.swift diff --git a/Sources/MarkdownView/Extensions/AnyShapeStyle+Equatable.swift b/Sources/MarkdownView/Extensions/AnyShapeStyle+Equatable.swift index 414940a3..7e650e86 100644 --- a/Sources/MarkdownView/Extensions/AnyShapeStyle+Equatable.swift +++ b/Sources/MarkdownView/Extensions/AnyShapeStyle+Equatable.swift @@ -1,6 +1,6 @@ import SwiftUI -extension AnyShapeStyle: Equatable { +extension AnyShapeStyle: @retroactive Equatable { public static func == (lhs: AnyShapeStyle, rhs: AnyShapeStyle) -> Bool { let oldBuffer = withUnsafeBytes(of: lhs) { $0 } let newBuffer = withUnsafeBytes(of: rhs) { $0 } diff --git a/Sources/MarkdownView/Helper/ActorIsolated.swift b/Sources/MarkdownView/Helper/ActorIsolated.swift new file mode 100644 index 00000000..e6fd53a3 --- /dev/null +++ b/Sources/MarkdownView/Helper/ActorIsolated.swift @@ -0,0 +1,9 @@ +import Foundation + +actor ActorIsolated { + public var value: Value + + init(_ value: Value) { + self.value = value + } +} diff --git a/Sources/MarkdownView/MarkdownView.swift b/Sources/MarkdownView/MarkdownView.swift index 15b1fb07..356bcf21 100644 --- a/Sources/MarkdownView/MarkdownView.swift +++ b/Sources/MarkdownView/MarkdownView.swift @@ -69,7 +69,7 @@ public struct MarkdownView: View { } .sizeOfView($viewSize) .containerSize(viewSize) - .updateCodeBlocksWhenColorSchemeChanges() + .modifier(CodeHighlighterUpdater()) .font(fontGroup.body) // Default font .if(renderingMode == .optimized && renderingThread == .background) { content in content diff --git a/Sources/MarkdownView/Renderer/CodeHighlight/CodeHighlighterUpdater.swift b/Sources/MarkdownView/Renderer/CodeHighlight/CodeHighlighterUpdater.swift new file mode 100644 index 00000000..e331315e --- /dev/null +++ b/Sources/MarkdownView/Renderer/CodeHighlight/CodeHighlighterUpdater.swift @@ -0,0 +1,36 @@ +#if canImport(Highlightr) +@preconcurrency import Highlightr +#endif +import SwiftUI + +/// A responder that update the theme of highlightr when environment value changes. +struct CodeHighlighterUpdater: ViewModifier { + @Environment(\.colorScheme) private var colorScheme + @Environment(\.codeHighlighterTheme) private var theme: CodeHighlighterTheme + + @State private var highlightrUpdateTaskCache: Task? + + func body(content: Content) -> some View { + content + #if canImport(Highlightr) + .onChange(of: colorScheme) { colorScheme in + highlightrUpdateTaskCache?.cancel() + highlightrUpdateTaskCache = Task { + let theme = colorScheme == .dark ? theme.darkModeThemeName : theme.lightModeThemeName + let highlighr = await Highlightr.shared.value + try Task.checkCancellation() + highlighr?.setTheme(to: theme) + } + } + .onChange(of: theme) { newTheme in + highlightrUpdateTaskCache?.cancel() + highlightrUpdateTaskCache = Task { + let theme = colorScheme == .dark ? newTheme.darkModeThemeName : newTheme.lightModeThemeName + let highlighr = await Highlightr.shared.value + try Task.checkCancellation() + highlighr?.setTheme(to: theme) + } + } + #endif + } +} diff --git a/Sources/MarkdownView/Renderer/CodeHighlight/Highlightr+Shared.swift b/Sources/MarkdownView/Renderer/CodeHighlight/Highlightr+Shared.swift new file mode 100644 index 00000000..ec29fe7c --- /dev/null +++ b/Sources/MarkdownView/Renderer/CodeHighlight/Highlightr+Shared.swift @@ -0,0 +1,9 @@ +#if canImport(Highlightr) +@preconcurrency import Highlightr +#endif + +#if canImport(Highlightr) +extension Highlightr { + static let shared: ActorIsolated = ActorIsolated(Highlightr()) +} +#endif diff --git a/Sources/MarkdownView/View/CodeBlockView.swift b/Sources/MarkdownView/View/CodeBlockView.swift index 5ee37780..2442aa70 100644 --- a/Sources/MarkdownView/View/CodeBlockView.swift +++ b/Sources/MarkdownView/View/CodeBlockView.swift @@ -16,7 +16,7 @@ struct HighlightedCodeBlock: View { @State private var showCopyButton = false private var id: String { - "\(colorScheme) mode" + (language ?? "No Language Name") + code + "\(colorScheme) mode" + (language ?? "Plain Text") + code } var body: some View { @@ -60,58 +60,21 @@ struct HighlightedCodeBlock: View { } } - private func highlight() { - guard let highlighter = Highlightr.shared else { return } - let language = self.language?.lowercased() - let originalCode = code + nonisolated private func highlight() async { + guard let highlighter = await Highlightr.shared.value else { return } + let specifiedLanguage = self.language?.lowercased() ?? "" - Task.detached { [language, highlighter] in - let language = highlighter.supportedLanguages() - .first(where: { $0.localizedLowercase == language }) - async let highlight = highlighter.highlight(originalCode, as: language) - guard let highlightedCode = await highlight else { return } - let attributedCode = AttributedString(highlightedCode) - - Task { @MainActor in - withAnimation { - self.attributedCode = attributedCode - } + let hLang = highlighter.supportedLanguages() + .first(where: { $0.localizedCaseInsensitiveCompare(specifiedLanguage) == .orderedSame }) + async let highlight = highlighter.highlight(code, as: hLang) + guard let highlightedCode = await highlight else { return } + let attributedCode = AttributedString(highlightedCode) + + await MainActor.run { + withAnimation { + self.attributedCode = attributedCode } } } } #endif - -// MARK: - Shared Instance - -#if canImport(Highlightr) -extension Highlightr { - @MainActor - static var shared: Highlightr? = Highlightr() -} -#endif - -struct CodeHighlighterUpdator: ViewModifier { - @Environment(\.colorScheme) private var colorScheme - @Environment(\.codeHighlighterTheme) private var theme: CodeHighlighterTheme - - func body(content: Content) -> some View { - content - #if canImport(Highlightr) - .task(id: colorScheme) { - let theme = colorScheme == .dark ? theme.darkModeThemeName : theme.lightModeThemeName - Highlightr.shared?.setTheme(to: theme) - } - .onChange(of: theme) { newTheme in - let theme = colorScheme == .dark ? newTheme.darkModeThemeName : newTheme.lightModeThemeName - Highlightr.shared?.setTheme(to: theme) - } - #endif - } -} - -extension View { - func updateCodeBlocksWhenColorSchemeChanges() -> some View { - modifier(CodeHighlighterUpdator()) - } -} diff --git a/Sources/MarkdownView/View/SVGView.swift b/Sources/MarkdownView/View/SVGView.swift index 16b0cf14..0ddefef4 100644 --- a/Sources/MarkdownView/View/SVGView.swift +++ b/Sources/MarkdownView/View/SVGView.swift @@ -34,6 +34,7 @@ struct SVGView: View { } // MARK: - WKWebView Delegate + @MainActor fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate { var updateSize: ((CGSize) -> Void)? @@ -108,8 +109,8 @@ fileprivate struct _SVGViewBridge: UIViewRepresentable { } } - func makeCoordinator() -> Coordinator { - Coordinator(updateSize: updateSize) + func makeCoordinator() -> WebViewDelegate { + WebViewDelegate(updateSize: updateSize) } } #endif From 641c321cbc1a71ddaafbef33b17767b6efd8146f Mon Sep 17 00:00:00 2001 From: Yanan Li <1474700628@qq.com> Date: Wed, 23 Oct 2024 15:58:22 +0800 Subject: [PATCH 4/4] add CI for build testing --- .github/workflows/build-test.yml | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/build-test.yml diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..c88b6979 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,103 @@ +name: Build for Apple Platforms + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +env: + PROJECT_SCHEME: MarkdownView # Set your project's scheme here + +jobs: + build-ios: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Build for iOS + run: | + xcodebuild clean build \ + -scheme ${{ env.PROJECT_SCHEME }} \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' + + build-macos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Build for macOS + run: | + xcodebuild clean build \ + -scheme ${{ env.PROJECT_SCHEME }} \ + -sdk macosx \ + -destination 'platform=OS X' + + build-tvos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Build for tvOS + run: | + xcodebuild clean build \ + -scheme ${{ env.PROJECT_SCHEME }} \ + -sdk appletvsimulator \ + -destination 'platform=tvOS Simulator,name=Apple TV' + + build-watchos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Build for watchOS + run: | + xcodebuild clean build \ + -scheme ${{ env.PROJECT_SCHEME }} \ + -sdk watchsimulator \ + -destination 'platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm)' + + build-visionos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + + - name: Build for visionOS + run: | + xcodebuild clean build \ + -scheme ${{ env.PROJECT_SCHEME }} \ + -sdk xros \ + -destination 'platform=visionOS Simulator,name=Apple Vision Pro'