diff --git a/Sources/MarkdownView/Caches/Cacheable.swift b/Sources/MarkdownView/Caches/Cacheable.swift index 1061343ae..3003ee8bd 100644 --- a/Sources/MarkdownView/Caches/Cacheable.swift +++ b/Sources/MarkdownView/Caches/Cacheable.swift @@ -8,8 +8,8 @@ import Foundation protocol Cacheable { - associatedtype CacheKey: Hashable - var cacheKey: CacheKey { get } + associatedtype Key: Hashable + var cacheKey: Key { get } init?(fromCache value: any Cacheable) } diff --git a/Sources/MarkdownView/Configurations/MarkdownListConfiguration.swift b/Sources/MarkdownView/Configurations/MarkdownListConfiguration.swift deleted file mode 100644 index 0867d0fb2..000000000 --- a/Sources/MarkdownView/Configurations/MarkdownListConfiguration.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// MarkdownListConfiguration.swift -// MarkdownView -// -// Created by LiYanan2004 on 2024/12/11. -// - -import Markdown -import Foundation - -struct MarkdownListConfiguration: Hashable, @unchecked Sendable { - var leadingIndentation: CGFloat = 12 - var unorderedListMarker = AnyUnorderedListMarkerProtocol(.bullet) - var orderedListMarker = AnyOrderedListMarkerProtocol(.increasingDigits) -} - -// MARK: - Ordered List Marker - -/// A type that represents the marker for ordered list items. -public protocol OrderedListMarkerProtocol: Hashable { - /// Returns a marker for a specific index of ordered list item. Index starting from 0. - func marker(at index: Int, listDepth: Int) -> String - - /// A boolean value indicates whether the marker should be monospaced, default value is `true`. - var monospaced: Bool { get } -} - -extension OrderedListMarkerProtocol { - public var monospaced: Bool { - true - } -} - -struct AnyOrderedListMarkerProtocol: OrderedListMarkerProtocol { - private var _marker: AnyHashable - var monospaced: Bool { - (_marker as! (any OrderedListMarkerProtocol)).monospaced - } - - init(_ marker: T) { - self._marker = AnyHashable(marker) - } - - public func marker(at index: Int, listDepth: Int) -> String { - (_marker as! (any OrderedListMarkerProtocol)).marker(at: index, listDepth: listDepth) - } -} - -/// An auto-increasing digits marker for ordered list items. -public struct OrderedListIncreasingDigitsMarker: OrderedListMarkerProtocol { - public func marker(at index: Int, listDepth: Int) -> String { - String(index + 1) + "." - } - - public var monospaced: Bool { false } -} - -extension OrderedListMarkerProtocol where Self == OrderedListIncreasingDigitsMarker { - /// An auto-increasing digits marker for ordered list items. - static public var increasingDigits: OrderedListIncreasingDigitsMarker { .init() } -} - -/// An auto-increasing letters marker for ordered list items. -public struct OrderedListIncreasingLettersMarker: OrderedListMarkerProtocol { - public func marker(at index: Int, listDepth: Int) -> String { - let base = 26 - var index = index - var result = "" - - // If index is smaller than 26, use single letter, otherwise, use double letters. - if index < base { - result = String(UnicodeScalar("a".unicodeScalars.first!.value + UInt32(index))!) - } else { - index -= base - let firstLetterIndex = index / base - let secondLetterIndex = index % base - let firstLetter = UnicodeScalar("a".unicodeScalars.first!.value + UInt32(firstLetterIndex))! - let secondLetter = UnicodeScalar("a".unicodeScalars.first!.value + UInt32(secondLetterIndex))! - result.append(Character(firstLetter)) - result.append(Character(secondLetter)) - } - - return result + "." - } - - public var monospaced: Bool { false } -} - -extension OrderedListMarkerProtocol where Self == OrderedListIncreasingLettersMarker { - /// An auto-increasing letters marker for ordered list items. - static public var increasingLetters: OrderedListIncreasingLettersMarker { .init() } -} - -// MARK: - Unordered List Marker - -/// A type that represents the marker for unordered list items. -public protocol UnorderedListMarkerProtocol: Hashable { - /// Returns a marker for a specific indentation level of unordered list item. indentationLevel starting from 0. - func marker(listDepth: Int) -> String - - /// A boolean value indicates whether the marker should be monospaced, default value is `true`. - var monospaced: Bool { get } -} - -extension UnorderedListMarkerProtocol { - public var monospaced: Bool { - true - } -} - -struct AnyUnorderedListMarkerProtocol: UnorderedListMarkerProtocol { - private var _marker: AnyHashable - var monospaced: Bool { - (_marker as! (any UnorderedListMarkerProtocol)).monospaced - } - - init(_ marker: T) { - self._marker = AnyHashable(marker) - } - - public func marker(listDepth: Int) -> String { - (_marker as! (any UnorderedListMarkerProtocol)).marker(listDepth: listDepth) - } -} - -/// A dash marker for unordered list items. -public struct UnorderedListDashMarker: UnorderedListMarkerProtocol { - public func marker(listDepth: Int) -> String { - "-" - } -} - -extension UnorderedListMarkerProtocol where Self == UnorderedListDashMarker { - /// A dash marker for unordered list items. - static public var dash: UnorderedListDashMarker { .init() } -} - -/// A bullet marker for unordered list items. -public struct UnorderedListBulletMarker: UnorderedListMarkerProtocol { - public func marker(listDepth: Int) -> String { - "•" - } -} - -extension UnorderedListMarkerProtocol where Self == UnorderedListBulletMarker { - /// A bullet marker for unordered list items. - static public var bullet: UnorderedListBulletMarker { .init() } -} diff --git a/Sources/MarkdownView/Configurations/MarkdownRendererConfiguration.Math.swift b/Sources/MarkdownView/Configurations/MarkdownRendererConfiguration.Math.swift deleted file mode 100644 index faac11ab7..000000000 --- a/Sources/MarkdownView/Configurations/MarkdownRendererConfiguration.Math.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MarkdownRendererConfiguration.Math.swift -// MarkdownView -// -// Created by LiYanan2004 on 2025/4/16. -// - -import Foundation - -extension MarkdownRendererConfiguration { - struct Math: Sendable, Hashable { - var shouldRender: Bool { - get { displayMathStorage != nil } - set(enabled) { - if enabled { - displayMathStorage = [:] - } else { - displayMathStorage = nil - } - } - } - var displayMathStorage: [UUID : String]? = nil - - mutating func appendDisplayMath(_ displayMath: some StringProtocol) -> UUID { - if displayMathStorage == nil { - displayMathStorage = [:] - } - - let id = UUID() - displayMathStorage![id] = String(displayMath) - return id - } - } -} diff --git a/Sources/MarkdownView/Configurations/MarkdownRendererConfiguration.swift b/Sources/MarkdownView/Configurations/MarkdownRendererConfiguration.swift deleted file mode 100644 index fe99a7450..000000000 --- a/Sources/MarkdownView/Configurations/MarkdownRendererConfiguration.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// MarkdownRendererConfiguration.swift -// MarkdownView -// -// Created by LiYanan2004 on 2024/12/11. -// - -import Foundation -import SwiftUI - -struct MarkdownRendererConfiguration: Equatable, AllowingModifyThroughKeyPath, Sendable { - var preferredBaseURL: URL? - var componentSpacing: CGFloat = 8 - - var math: Math = Math() - - var linkTintColor: Color = .accentColor - var inlineCodeTintColor: Color = .accentColor - var blockQuoteTintColor: Color = .accentColor - - var listConfiguration: MarkdownListConfiguration = MarkdownListConfiguration() - - var allowedImageRenderers: Set = ["https", "http"] - var allowedBlockDirectiveRenderers: Set = [] -} - -// MARK: - SwiftUI Environment - -struct MarkdownRendererConfigurationKey: EnvironmentKey { - static let defaultValue: MarkdownRendererConfiguration = .init() -} - -extension EnvironmentValues { - var markdownRendererConfiguration: MarkdownRendererConfiguration { - get { self[MarkdownRendererConfigurationKey.self] } - set { self[MarkdownRendererConfigurationKey.self] = newValue } - } -} diff --git a/Sources/MarkdownView/Renderers/Node Representations/Block Quotes/Protocol/BlockQuoteStyle.swift b/Sources/MarkdownView/Customizations/Block Quotes/BlockQuoteStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Block Quotes/Protocol/BlockQuoteStyle.swift rename to Sources/MarkdownView/Customizations/Block Quotes/BlockQuoteStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Block Quotes/Styles/DefaultBlockQuoteStyle.swift b/Sources/MarkdownView/Customizations/Block Quotes/DefaultBlockQuoteStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Block Quotes/Styles/DefaultBlockQuoteStyle.swift rename to Sources/MarkdownView/Customizations/Block Quotes/DefaultBlockQuoteStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Block Quotes/Styles/GithubBlockQuoteStyle.swift b/Sources/MarkdownView/Customizations/Block Quotes/GithubBlockQuoteStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Block Quotes/Styles/GithubBlockQuoteStyle.swift rename to Sources/MarkdownView/Customizations/Block Quotes/GithubBlockQuoteStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Code Blocks/Protocol/CodeBlockStyle.swift b/Sources/MarkdownView/Customizations/Code Blocks/CodeBlockStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Code Blocks/Protocol/CodeBlockStyle.swift rename to Sources/MarkdownView/Customizations/Code Blocks/CodeBlockStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Code Blocks/Styles/DefaultCodeBlockStyle.swift b/Sources/MarkdownView/Customizations/Code Blocks/DefaultCodeBlockStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Code Blocks/Styles/DefaultCodeBlockStyle.swift rename to Sources/MarkdownView/Customizations/Code Blocks/DefaultCodeBlockStyle.swift diff --git a/Sources/MarkdownView/Configurations/CodeHighlighterTheme.swift b/Sources/MarkdownView/Customizations/CodeHighlighterTheme.swift similarity index 100% rename from Sources/MarkdownView/Configurations/CodeHighlighterTheme.swift rename to Sources/MarkdownView/Customizations/CodeHighlighterTheme.swift diff --git a/Sources/MarkdownView/Configurations/Font/AnyMarkdownFontGroup.swift b/Sources/MarkdownView/Customizations/Font/AnyMarkdownFontGroup.swift similarity index 100% rename from Sources/MarkdownView/Configurations/Font/AnyMarkdownFontGroup.swift rename to Sources/MarkdownView/Customizations/Font/AnyMarkdownFontGroup.swift diff --git a/Sources/MarkdownView/Configurations/Font/DefaultFontGroup.swift b/Sources/MarkdownView/Customizations/Font/DefaultFontGroup.swift similarity index 100% rename from Sources/MarkdownView/Configurations/Font/DefaultFontGroup.swift rename to Sources/MarkdownView/Customizations/Font/DefaultFontGroup.swift diff --git a/Sources/MarkdownView/Configurations/Font/MarkdownFontGroup.swift b/Sources/MarkdownView/Customizations/Font/MarkdownFontGroup.swift similarity index 100% rename from Sources/MarkdownView/Configurations/Font/MarkdownFontGroup.swift rename to Sources/MarkdownView/Customizations/Font/MarkdownFontGroup.swift diff --git a/Sources/MarkdownView/Configurations/Font/MarkdownTextType.swift b/Sources/MarkdownView/Customizations/Font/MarkdownTextType.swift similarity index 100% rename from Sources/MarkdownView/Configurations/Font/MarkdownTextType.swift rename to Sources/MarkdownView/Customizations/Font/MarkdownTextType.swift diff --git a/Sources/MarkdownView/Configurations/ForegroundStyle/AnyForegroundStyleGroup.swift b/Sources/MarkdownView/Customizations/Foreground Styles/AnyForegroundStyleGroup.swift similarity index 100% rename from Sources/MarkdownView/Configurations/ForegroundStyle/AnyForegroundStyleGroup.swift rename to Sources/MarkdownView/Customizations/Foreground Styles/AnyForegroundStyleGroup.swift diff --git a/Sources/MarkdownView/Configurations/ForegroundStyle/AutomaticForegroundStyleGroup.swift b/Sources/MarkdownView/Customizations/Foreground Styles/AutomaticForegroundStyleGroup.swift similarity index 100% rename from Sources/MarkdownView/Configurations/ForegroundStyle/AutomaticForegroundStyleGroup.swift rename to Sources/MarkdownView/Customizations/Foreground Styles/AutomaticForegroundStyleGroup.swift diff --git a/Sources/MarkdownView/Configurations/ForegroundStyle/HeadingStyleGroup.swift b/Sources/MarkdownView/Customizations/Foreground Styles/HeadingStyleGroup.swift similarity index 100% rename from Sources/MarkdownView/Configurations/ForegroundStyle/HeadingStyleGroup.swift rename to Sources/MarkdownView/Customizations/Foreground Styles/HeadingStyleGroup.swift diff --git a/Sources/MarkdownView/Configurations/ForegroundStyle/MarkdownStyleTarget.swift b/Sources/MarkdownView/Customizations/Foreground Styles/MarkdownStyleTarget.swift similarity index 100% rename from Sources/MarkdownView/Configurations/ForegroundStyle/MarkdownStyleTarget.swift rename to Sources/MarkdownView/Customizations/Foreground Styles/MarkdownStyleTarget.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Heading/Configurations/HeadingLevel.swift b/Sources/MarkdownView/Customizations/Heading/HeadingLevel.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Heading/Configurations/HeadingLevel.swift rename to Sources/MarkdownView/Customizations/Heading/HeadingLevel.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Heading/Configurations/HeadingPaddings.swift b/Sources/MarkdownView/Customizations/Heading/HeadingPaddings.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Heading/Configurations/HeadingPaddings.swift rename to Sources/MarkdownView/Customizations/Heading/HeadingPaddings.swift diff --git a/Sources/MarkdownView/Customizations/List/Ordered List Marker/AnyOrderedListMarkerProtocol.swift b/Sources/MarkdownView/Customizations/List/Ordered List Marker/AnyOrderedListMarkerProtocol.swift new file mode 100644 index 000000000..ea957c3cf --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Ordered List Marker/AnyOrderedListMarkerProtocol.swift @@ -0,0 +1,23 @@ +// +// AnyOrderedListMarkerProtocol.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +struct AnyOrderedListMarkerProtocol: OrderedListMarkerProtocol { + private var _marker: AnyHashable + var monospaced: Bool { + (_marker as! (any OrderedListMarkerProtocol)).monospaced + } + + init(_ marker: T) { + self._marker = AnyHashable(marker) + } + + public func marker(at index: Int, listDepth: Int) -> String { + (_marker as! (any OrderedListMarkerProtocol)).marker(at: index, listDepth: listDepth) + } +} diff --git a/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListIncreasingDigitsMarker.swift b/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListIncreasingDigitsMarker.swift new file mode 100644 index 000000000..1fd3feb06 --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListIncreasingDigitsMarker.swift @@ -0,0 +1,22 @@ +// +// OrderedListIncreasingDigitsMarker.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +/// An auto-increasing digits marker for ordered list items. +public struct OrderedListIncreasingDigitsMarker: OrderedListMarkerProtocol { + public func marker(at index: Int, listDepth: Int) -> String { + String(index + 1) + "." + } + + public var monospaced: Bool { false } +} + +extension OrderedListMarkerProtocol where Self == OrderedListIncreasingDigitsMarker { + /// An auto-increasing digits marker for ordered list items. + static public var increasingDigits: OrderedListIncreasingDigitsMarker { .init() } +} diff --git a/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListIncreasingLettersMarker.swift b/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListIncreasingLettersMarker.swift new file mode 100644 index 000000000..29d453ddb --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListIncreasingLettersMarker.swift @@ -0,0 +1,39 @@ +// +// OrderedListIncreasingLettersMarker.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +/// An auto-increasing letters marker for ordered list items. +public struct OrderedListIncreasingLettersMarker: OrderedListMarkerProtocol { + public func marker(at index: Int, listDepth: Int) -> String { + let base = 26 + var index = index + var result = "" + + // If index is smaller than 26, use single letter, otherwise, use double letters. + if index < base { + result = String(UnicodeScalar("a".unicodeScalars.first!.value + UInt32(index))!) + } else { + index -= base + let firstLetterIndex = index / base + let secondLetterIndex = index % base + let firstLetter = UnicodeScalar("a".unicodeScalars.first!.value + UInt32(firstLetterIndex))! + let secondLetter = UnicodeScalar("a".unicodeScalars.first!.value + UInt32(secondLetterIndex))! + result.append(Character(firstLetter)) + result.append(Character(secondLetter)) + } + + return result + "." + } + + public var monospaced: Bool { false } +} + +extension OrderedListMarkerProtocol where Self == OrderedListIncreasingLettersMarker { + /// An auto-increasing letters marker for ordered list items. + static public var increasingLetters: OrderedListIncreasingLettersMarker { .init() } +} diff --git a/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListMarkerProtocol.swift b/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListMarkerProtocol.swift new file mode 100644 index 000000000..42102c64b --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Ordered List Marker/OrderedListMarkerProtocol.swift @@ -0,0 +1,23 @@ +// +// OrderedListMarkerProtocol.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +/// A type that represents the marker for ordered list items. +public protocol OrderedListMarkerProtocol: Hashable { + /// Returns a marker for a specific index of ordered list item. Index starting from 0. + func marker(at index: Int, listDepth: Int) -> String + + /// A boolean value indicates whether the marker should be monospaced, default value is `true`. + var monospaced: Bool { get } +} + +extension OrderedListMarkerProtocol { + public var monospaced: Bool { + true + } +} diff --git a/Sources/MarkdownView/Customizations/List/Unordered List Marker/AnyUnorderedListMarkerProtocol.swift b/Sources/MarkdownView/Customizations/List/Unordered List Marker/AnyUnorderedListMarkerProtocol.swift new file mode 100644 index 000000000..5e99a7aa9 --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Unordered List Marker/AnyUnorderedListMarkerProtocol.swift @@ -0,0 +1,23 @@ +// +// AnyUnorderedListMarkerProtocol.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +struct AnyUnorderedListMarkerProtocol: UnorderedListMarkerProtocol { + private var _marker: AnyHashable + var monospaced: Bool { + (_marker as! (any UnorderedListMarkerProtocol)).monospaced + } + + init(_ marker: T) { + self._marker = AnyHashable(marker) + } + + public func marker(listDepth: Int) -> String { + (_marker as! (any UnorderedListMarkerProtocol)).marker(listDepth: listDepth) + } +} diff --git a/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListBulletMarker.swift b/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListBulletMarker.swift new file mode 100644 index 000000000..6e1ed1c34 --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListBulletMarker.swift @@ -0,0 +1,20 @@ +// +// UnorderedListBulletMarker.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +/// A bullet marker for unordered list items. +public struct UnorderedListBulletMarker: UnorderedListMarkerProtocol { + public func marker(listDepth: Int) -> String { + "•" + } +} + +extension UnorderedListMarkerProtocol where Self == UnorderedListBulletMarker { + /// A bullet marker for unordered list items. + static public var bullet: UnorderedListBulletMarker { .init() } +} diff --git a/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListDashMarker.swift b/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListDashMarker.swift new file mode 100644 index 000000000..08107f8eb --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListDashMarker.swift @@ -0,0 +1,20 @@ +// +// UnorderedListDashMarker.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +/// A dash marker for unordered list items. +public struct UnorderedListDashMarker: UnorderedListMarkerProtocol { + public func marker(listDepth: Int) -> String { + "-" + } +} + +extension UnorderedListMarkerProtocol where Self == UnorderedListDashMarker { + /// A dash marker for unordered list items. + static public var dash: UnorderedListDashMarker { .init() } +} diff --git a/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListMarkerProtocol.swift b/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListMarkerProtocol.swift new file mode 100644 index 000000000..3e3d06a2d --- /dev/null +++ b/Sources/MarkdownView/Customizations/List/Unordered List Marker/UnorderedListMarkerProtocol.swift @@ -0,0 +1,23 @@ +// +// UnorderedListMarkerProtocol.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation + +/// A type that represents the marker for unordered list items. +public protocol UnorderedListMarkerProtocol: Hashable { + /// Returns a marker for a specific indentation level of unordered list item. indentationLevel starting from 0. + func marker(listDepth: Int) -> String + + /// A boolean value indicates whether the marker should be monospaced, default value is `true`. + var monospaced: Bool { get } +} + +extension UnorderedListMarkerProtocol { + public var monospaced: Bool { + true + } +} diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.Fallback.swift b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.Fallback.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.Fallback.swift rename to Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.Fallback.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.Header.swift b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.Header.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.Header.swift rename to Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.Header.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.Row.swift b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.Row.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.Row.swift rename to Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.Row.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.swift b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.Table.swift rename to Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.Table.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.swift b/Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/Configuration/MarkdownTableStyleConfiguration.swift rename to Sources/MarkdownView/Customizations/Table/Configuration/MarkdownTableStyleConfiguration.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Styles/DefaultMarkdownTableStyle.swift b/Sources/MarkdownView/Customizations/Table/DefaultMarkdownTableStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Styles/DefaultMarkdownTableStyle.swift rename to Sources/MarkdownView/Customizations/Table/DefaultMarkdownTableStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Styles/GithubMarkdownTableStyle.swift b/Sources/MarkdownView/Customizations/Table/GithubMarkdownTableStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Styles/GithubMarkdownTableStyle.swift rename to Sources/MarkdownView/Customizations/Table/GithubMarkdownTableStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Styles/GridMarkdownTableStyle.swift b/Sources/MarkdownView/Customizations/Table/GridMarkdownTableStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Styles/GridMarkdownTableStyle.swift rename to Sources/MarkdownView/Customizations/Table/GridMarkdownTableStyle.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/MarkdownTableStyle.swift b/Sources/MarkdownView/Customizations/Table/MarkdownTableStyle.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Tables/Protocol/MarkdownTableStyle.swift rename to Sources/MarkdownView/Customizations/Table/MarkdownTableStyle.swift diff --git a/Sources/MarkdownView/DeprecatedAPIs.swift b/Sources/MarkdownView/Deprecated & Unavailable APIs/v2/DeprecatedAPIs.swift similarity index 100% rename from Sources/MarkdownView/DeprecatedAPIs.swift rename to Sources/MarkdownView/Deprecated & Unavailable APIs/v2/DeprecatedAPIs.swift diff --git a/Sources/MarkdownView/Modifiers/MarkdownViewStyle.swift b/Sources/MarkdownView/Deprecated & Unavailable APIs/v2/MarkdownViewStyle.swift similarity index 82% rename from Sources/MarkdownView/Modifiers/MarkdownViewStyle.swift rename to Sources/MarkdownView/Deprecated & Unavailable APIs/v2/MarkdownViewStyle.swift index 0f2e40ecb..7385090a1 100644 --- a/Sources/MarkdownView/Modifiers/MarkdownViewStyle.swift +++ b/Sources/MarkdownView/Deprecated & Unavailable APIs/v2/MarkdownViewStyle.swift @@ -8,6 +8,7 @@ import SwiftUI extension View { + @available(*, deprecated, message: "Wrap `MarkdownView` or apply modifiers directly at here. If you have already created a `MarkdownViewStyle`, just copy the code from `makeBody(configuration:) -> Body` and copy to here.") nonisolated public func markdownViewStyle(_ style: some MarkdownViewStyle) -> some View { environment(\.markdownViewStyle, style) } @@ -18,6 +19,7 @@ extension View { /// The appearance and layout behavior of MarkdownView. @MainActor @preconcurrency +@available(*, deprecated, message: "Use `ViewModifier` protocol instead.") public protocol MarkdownViewStyle { /// The properties of a MarkdownView. typealias Configuration = MarkdownViewStyleConfiguration @@ -47,12 +49,14 @@ public struct MarkdownViewStyleConfiguration { // MARK: - DefaultMarkdownViewStyle /// A MarkdownViewStyle that uses default appearances. +@available(*, deprecated) public struct DefaultMarkdownViewStyle: MarkdownViewStyle { public func makeBody(configuration: Configuration) -> some View { configuration.body } } +@available(*, deprecated) extension MarkdownViewStyle where Self == DefaultMarkdownViewStyle { /// A MarkdownViewStyle that uses default appearances. static public var `default`: DefaultMarkdownViewStyle { .init() } @@ -61,6 +65,7 @@ extension MarkdownViewStyle where Self == DefaultMarkdownViewStyle { // MARK: - EditorMarkdownViewStyle /// A MarkdownViewStyle that takes up all available spaces and align its content to top-leading, just like an editor. +@available(*, deprecated, message: "Use `.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)` instead.") public struct EditorMarkdownViewStyle: MarkdownViewStyle { public func makeBody(configuration: Configuration) -> some View { configuration.body @@ -68,6 +73,7 @@ public struct EditorMarkdownViewStyle: MarkdownViewStyle { } } +@available(*, deprecated) extension MarkdownViewStyle where Self == EditorMarkdownViewStyle { /// A MarkdownViewStyle that takes up all available spaces and align its content to top-leading, just like an editor. static public var editor: EditorMarkdownViewStyle { .init() } @@ -75,11 +81,13 @@ extension MarkdownViewStyle where Self == EditorMarkdownViewStyle { // MARK: - MarkdownViewStyle + Environment +@available(*, deprecated) struct MarkdownViewStyleEnvironmentKey: @preconcurrency EnvironmentKey { @MainActor static var defaultValue: any MarkdownViewStyle = .default } extension EnvironmentValues { + @available(*, deprecated) var markdownViewStyle: any MarkdownViewStyle { get { self[MarkdownViewStyleEnvironmentKey.self] } set { self[MarkdownViewStyleEnvironmentKey.self] = newValue } diff --git a/Sources/MarkdownView/Modifiers/RenderingModeModifier.swift b/Sources/MarkdownView/Deprecated & Unavailable APIs/v2/RenderingModeModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/RenderingModeModifier.swift rename to Sources/MarkdownView/Deprecated & Unavailable APIs/v2/RenderingModeModifier.swift diff --git a/Sources/MarkdownView/Extensions/Swift/ActorIsolated.swift b/Sources/MarkdownView/Extensions/Swift/ActorIsolated.swift deleted file mode 100644 index 8cdd59e6d..000000000 --- a/Sources/MarkdownView/Extensions/Swift/ActorIsolated.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -actor ActorIsolated { - var value: Value - - init(_ value: Value) { - self.value = value - } -} diff --git a/Sources/MarkdownView/Helpers/SwiftUI/AnyShape.swift b/Sources/MarkdownView/Helpers/Backward Capabilities/AnyShape.swift similarity index 100% rename from Sources/MarkdownView/Helpers/SwiftUI/AnyShape.swift rename to Sources/MarkdownView/Helpers/Backward Capabilities/AnyShape.swift diff --git a/Sources/MarkdownView/Helpers/View Modifiers/OnValueChange.swift b/Sources/MarkdownView/Helpers/Backward Capabilities/View+OnValueChange.swift similarity index 99% rename from Sources/MarkdownView/Helpers/View Modifiers/OnValueChange.swift rename to Sources/MarkdownView/Helpers/Backward Capabilities/View+OnValueChange.swift index a048629f5..5cec79efe 100644 --- a/Sources/MarkdownView/Helpers/View Modifiers/OnValueChange.swift +++ b/Sources/MarkdownView/Helpers/Backward Capabilities/View+OnValueChange.swift @@ -120,7 +120,7 @@ extension View { } } -// MARK: - Auxiliary +// MARK: - _BackDeployedOnChangeViewModifier fileprivate struct _BackDeployedOnChangeViewModifier: ViewModifier { nonisolated(unsafe) private var value: Value diff --git a/Sources/MarkdownView/Helpers/KeyPathModifiable.swift b/Sources/MarkdownView/Helpers/KeyPathModifiable.swift new file mode 100644 index 000000000..b908fa361 --- /dev/null +++ b/Sources/MarkdownView/Helpers/KeyPathModifiable.swift @@ -0,0 +1,27 @@ +// +// KeyPathModifiable.swift +// MarkdownView +// +// Created by Yanan Li on 2025/2/9. +// + +import Foundation + +protocol KeyPathModifiable { } + +extension KeyPathModifiable { + public func with(_ keyPath: WritableKeyPath, _ newValue: T) -> Self { + var copy = self + copy[keyPath: keyPath] = newValue + return copy + } + + public mutating func modify( + _ keyPath: WritableKeyPath, + _ modify: @escaping (inout T) -> Void + ) { + var value = self[keyPath: keyPath] + defer { self[keyPath: keyPath] = value } + modify(&value) + } +} diff --git a/Sources/MarkdownView/Extensions/Markdown/BlockQuote.swift b/Sources/MarkdownView/Helpers/Markdown/BlockQuote.swift similarity index 100% rename from Sources/MarkdownView/Extensions/Markdown/BlockQuote.swift rename to Sources/MarkdownView/Helpers/Markdown/BlockQuote.swift diff --git a/Sources/MarkdownView/Extensions/Markdown/ListItemContainer.swift b/Sources/MarkdownView/Helpers/Markdown/ListItemContainer.swift similarity index 100% rename from Sources/MarkdownView/Extensions/Markdown/ListItemContainer.swift rename to Sources/MarkdownView/Helpers/Markdown/ListItemContainer.swift diff --git a/Sources/MarkdownView/Extensions/Markdown/Markdown+Sendable.swift b/Sources/MarkdownView/Helpers/Markdown/Markdown+Sendable.swift similarity index 100% rename from Sources/MarkdownView/Extensions/Markdown/Markdown+Sendable.swift rename to Sources/MarkdownView/Helpers/Markdown/Markdown+Sendable.swift diff --git a/Sources/MarkdownView/Extensions/Markdown/Markup.swift b/Sources/MarkdownView/Helpers/Markdown/Markup.swift similarity index 100% rename from Sources/MarkdownView/Extensions/Markdown/Markup.swift rename to Sources/MarkdownView/Helpers/Markdown/Markup.swift diff --git a/Sources/MarkdownView/Extensions/Markdown/SourceLocation.swift b/Sources/MarkdownView/Helpers/Markdown/SourceLocation.swift similarity index 100% rename from Sources/MarkdownView/Extensions/Markdown/SourceLocation.swift rename to Sources/MarkdownView/Helpers/Markdown/SourceLocation.swift diff --git a/Sources/MarkdownView/Extensions/Markdown/Table++.swift b/Sources/MarkdownView/Helpers/Markdown/Table++.swift similarity index 100% rename from Sources/MarkdownView/Extensions/Markdown/Table++.swift rename to Sources/MarkdownView/Helpers/Markdown/Table++.swift diff --git a/Sources/MarkdownView/Helpers/MarkdownContent.swift b/Sources/MarkdownView/Helpers/MarkdownContent.swift deleted file mode 100644 index 14ea079d7..000000000 --- a/Sources/MarkdownView/Helpers/MarkdownContent.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// MarkdownContent.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/9. -// - -import Foundation -@preconcurrency import Markdown - -// MARK: - Raw - -enum RawMarkdownContent: Sendable, Hashable { - case plainText(String) - case url(URL) - - public var text: String { - switch self { - case .plainText(let text): - return text - case .url(let url): - return (try? String(contentsOf: url)) ?? "" - } - } - - public var source: URL? { - if case .url(let url) = self { - return url - } - return nil - } -} - -// MARK: - Parsed Content - -/// A Sendable markdown content that can be used to render content and supports on-demand parsing. -public struct MarkdownContent: Sendable { - var raw: RawMarkdownContent - - class ParsedDocumentStore: /* NSLock */ @unchecked Sendable { - private var lock = NSLock() - private var caches: [ParseOptions.RawValue : Document] = [:] - - fileprivate func parse(_ rawContent: RawMarkdownContent, options: ParseOptions = ParseOptions()) -> Document { - lock.lock() - defer { lock.unlock() } - - if let cached = caches[options.rawValue] { - return cached - } - - let document = Document( - parsing: rawContent.text, - source: rawContent.source, - options: options - ) - caches[options.rawValue] = document - return document - } - - var documents: LazySequence.Values> { - lock.withLock { - caches.values.lazy - } - } - - var hasParsedDocument: Bool { - !documents.isEmpty - } - } - var store: ParsedDocumentStore - - internal init(raw: RawMarkdownContent) { - self.raw = raw - self.store = ParsedDocumentStore() - } - - func parse(options: ParseOptions = ParseOptions()) -> Document { - store.parse(raw, options: options) - } -} - -extension MarkdownContent: Hashable { - public static func == (lhs: MarkdownContent, rhs: MarkdownContent) -> Bool { - lhs.raw == rhs.raw - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(raw) - } -} diff --git a/Sources/MarkdownView/Helpers/Protocols/AllowingModifyThroughKeyPath.swift b/Sources/MarkdownView/Helpers/Protocols/AllowingModifyThroughKeyPath.swift deleted file mode 100644 index 6caf80f81..000000000 --- a/Sources/MarkdownView/Helpers/Protocols/AllowingModifyThroughKeyPath.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AllowingModifyThroughKeyPath.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/9. -// - -import Foundation - -protocol AllowingModifyThroughKeyPath { } - -extension AllowingModifyThroughKeyPath { - public func with(_ keyPath: WritableKeyPath, _ newValue: T) -> Self { - var copy = self - copy[keyPath: keyPath] = newValue - return copy - } -} diff --git a/Sources/MarkdownView/Helpers/Layouts/FlowLayout.swift b/Sources/MarkdownView/Helpers/SwiftUI/FlowLayout.swift similarity index 99% rename from Sources/MarkdownView/Helpers/Layouts/FlowLayout.swift rename to Sources/MarkdownView/Helpers/SwiftUI/FlowLayout.swift index f9c294054..d71178e5c 100644 --- a/Sources/MarkdownView/Helpers/Layouts/FlowLayout.swift +++ b/Sources/MarkdownView/Helpers/SwiftUI/FlowLayout.swift @@ -106,4 +106,3 @@ extension FlowLayout { var size: CGSize } } - diff --git a/Sources/MarkdownView/Extensions/SwiftUI/Image++.swift b/Sources/MarkdownView/Helpers/SwiftUI/Image++.swift similarity index 100% rename from Sources/MarkdownView/Extensions/SwiftUI/Image++.swift rename to Sources/MarkdownView/Helpers/SwiftUI/Image++.swift diff --git a/Sources/MarkdownView/Extensions/SwiftUI/View++.swift b/Sources/MarkdownView/Helpers/SwiftUI/View++.swift similarity index 100% rename from Sources/MarkdownView/Extensions/SwiftUI/View++.swift rename to Sources/MarkdownView/Helpers/SwiftUI/View++.swift diff --git a/Sources/MarkdownView/Helpers/Text Manipulations/TextBuilder.swift b/Sources/MarkdownView/Helpers/Text Manipulations/TextBuilder.swift deleted file mode 100644 index f54d7f875..000000000 --- a/Sources/MarkdownView/Helpers/Text Manipulations/TextBuilder.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// TextBuilder.swift -// MarkdownView -// -// Created by Yanan Li on 2025/4/13. -// - -import SwiftUI - -@resultBuilder -struct TextBuilder { - static func buildBlock(_ components: Text...) -> Text { - components.reduce(Text(verbatim: ""), +) - } - - static func buildArray(_ components: [Text]) -> Text { - components.reduce(Text(verbatim: ""), +) - } - - static func buildOptional(_ component: Text?) -> Text { - if let component { - return component - } - return Text(verbatim: "") - } - - static func buildExpression(_ expression: Image) -> Text { - Text(expression) - } - - static func buildExpression(_ expression: Text) -> Text { - expression - } - - static func buildPartialBlock(accumulated: Text, next: Text) -> Text { - accumulated + next - } - - static func buildPartialBlock(first: Text) -> Text { - first - } - - static func buildEither(first component: Text) -> Text { - component - } - - static func buildEither(second component: Text) -> Text { - component - } -} diff --git a/Sources/MarkdownView/MarkdownContent.swift b/Sources/MarkdownView/MarkdownContent.swift new file mode 100644 index 000000000..f72b8054d --- /dev/null +++ b/Sources/MarkdownView/MarkdownContent.swift @@ -0,0 +1,152 @@ +// +// MarkdownContent.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/27. +// + +import Foundation +import Combine +@preconcurrency import Markdown + +/// An observable object that manages the content of the markdown and provides the parsed document. +public final class MarkdownContent: ObservableObject { + var store: ParsedDocumentStore! + + @Published private var raw: Raw { + willSet { + if raw != newValue { + store.resetStorage() + } + } + } + + /// The markdown text. + public var markdown: String { + get throws { + try raw.markdownText + } + } + + internal init(_ source: Raw) { + self.raw = source + self.store = ParsedDocumentStore(self) + } + + /// Parsed document. + /// + /// - parameter parseOptions: The parse options to use for markdown parsing or `nil` if you want to either use any cached version or parse the markdown with default options. + /// + /// This API try to find parsed document with given `parseOptions` in the cache. + /// If there is no matches, then it tries to parse the content and cache it for future query. + /// + /// If the `parseOptions` is set to `nil` and there is any cached document available, the first cached result will be returned. + internal func document(options: ParseOptions = ParseOptions()) -> Document { + store.parse(options: options) + } +} + +extension MarkdownContent { + /// Creates an instance from a plain string. + public convenience init(_ text: String) { + self.init(.plainText(text)) + } + + /// Creates an instance whose contents are loaded from a URL. + public convenience init(_ url: URL) { + self.init(.url(url)) + } + + /// Updates the source of the markdown content. + /// - parameter content: The markdown text. + public func updateContent(_ content: String) { + self.raw = .plainText(content) + } + + /// Updates the source of the markdown content. + /// - parameter content: The URL of the markdown file. + public func updateContent(_ content: URL) { + self.raw = .url(content) + } +} + +extension MarkdownContent { + class ParsedDocumentStore: /* NSLock */ @unchecked Sendable { + private var lock = NSLock() + private var caches: [ParseOptions.RawValue : Document] = [:] + unowned var content: MarkdownContent + + init(_ content: MarkdownContent) { + self.content = content + } + + fileprivate func parse( + options: ParseOptions = ParseOptions() + ) -> Document { + lock.lock() + defer { lock.unlock() } + + if let cached = caches[options.rawValue] { + return cached + } + + let text: String + do { + text = try content.markdown + } catch { + text = "" + logger.error("Unable to retrieve markdown content in string format: \(error). (fallback to empty string).") + } + + let document = Document( + parsing: text, + source: nil, + options: options + ) + caches[options.rawValue] = document + return document + } + + var documents: LazySequence.Values> { + lock.withLock { + caches.values.lazy + } + } + + var hasParsedDocument: Bool { + !documents.isEmpty + } + + func resetStorage() { + lock.lock() + defer { lock.unlock() } + + caches = [:] + } + } +} + +extension MarkdownContent: ExpressibleByStringLiteral { + public convenience init(stringLiteral value: String) { + self.init(value) + } +} + +extension MarkdownContent { + /// A representation of where the Markdown originates. + public enum Raw: Hashable { + case plainText(String) + case url(URL) + + public var markdownText: String { + get throws { + switch self { + case .plainText(let string): + string + case .url(let url): + try String(contentsOf: url, encoding: .utf8) + } + } + } + } +} diff --git a/Sources/MarkdownView/MarkdownReader.swift b/Sources/MarkdownView/MarkdownReader.swift index 610865d5c..6f9350d28 100644 --- a/Sources/MarkdownView/MarkdownReader.swift +++ b/Sources/MarkdownView/MarkdownReader.swift @@ -7,9 +7,10 @@ import SwiftUI -/// A reader that provides a markdown content to use across multiple views. +/// A reader that provides markdown content to use across multiple views. /// -/// This reader offers a single source-of-truth for its child markdown views, and ensures the input is only parsed once. +/// This reader offers a single source of truth for its child markdown views so +/// the same Markdown source flows through the hierarchy. /// /// ```swift /// MarkdownReader("**Hello World**") { markdown in @@ -20,22 +21,27 @@ import SwiftUI /// } /// ``` public struct MarkdownReader: View { - private var markdownContent: MarkdownContent - private var contents: (_ markdownContent: MarkdownContent) -> Content + @ObservedObject private var content: MarkdownContent + private var _body: (_ markdownContent: MarkdownContent) -> Content - public init(_ text: String, @ViewBuilder contents: @escaping (MarkdownContent) -> Content) { - self.markdownContent = MarkdownContent(raw: .plainText(text)) - self.contents = contents + public init( + _ text: String, + @ViewBuilder contents: @escaping (MarkdownContent) -> Content + ) { + content = MarkdownContent(text) + self._body = contents } - @_spi(WIP) - public init(_ url: URL, @ViewBuilder contents: @escaping (MarkdownContent) -> Content) { - self.markdownContent = MarkdownContent(raw: .url(url)) - self.contents = contents + public init( + _ url: URL, + @ViewBuilder contents: @escaping (MarkdownContent) -> Content + ) { + content = MarkdownContent(url) + self._body = contents } public var body: some View { - contents(markdownContent) + _body(content) } } diff --git a/Sources/MarkdownView/MarkdownTableOfContent.swift b/Sources/MarkdownView/MarkdownTableOfContent.swift deleted file mode 100644 index 592e8d554..000000000 --- a/Sources/MarkdownView/MarkdownTableOfContent.swift +++ /dev/null @@ -1,74 +0,0 @@ -import SwiftUI -import Markdown - -/// A customized view that defines its content as a function of a set of headings. -/// -/// You should use ``MarkdownView/MarkdownReader`` to provide single source-of-truth for MarkdownView and table of content. -public struct MarkdownTableOfContent: View { - private var markdownContent: MarkdownContent - private var contents: (_ headings: [MarkdownHeading]) -> Content - - public init( - _ markdownContent: MarkdownContent, - @ViewBuilder contents: @escaping ([MarkdownHeading]) -> Content - ) { - self.markdownContent = markdownContent - self.contents = contents - } - - private var headings: [MarkdownHeading] { - var toc = TableOfContentVisitor() - toc.visit( - markdownContent.store.documents.first ?? markdownContent.parse() - ) - return toc.headings - } - - public var body: some View { - contents(headings) - } -} - -extension MarkdownTableOfContent { - /// A representation of a markdown heading. - public struct MarkdownHeading: Hashable, Sendable { - private var heading: Markdown.Heading - - /// Heading level, starting from 1. - public var level: Int { - heading.level - } - /// The range of the heading in the raw Markdown. - public var range: SourceRange? { - heading.range - } - /// The content text of the heading. - public var plainText: String { - heading.plainText - } - - init(heading: Heading) { - self.heading = heading - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(level) - hasher.combine(range) - hasher.combine(plainText) - } - - public static func == (lhs: MarkdownHeading, rhs: MarkdownHeading) -> Bool { - lhs.heading.isIdentical(to: rhs.heading) - } - } - - struct TableOfContentVisitor: MarkupWalker { - private(set) var headings: [MarkdownHeading] = [] - - mutating func visitHeading(_ heading: Markdown.Heading) { - headings.append(MarkdownHeading(heading: heading)) - descendInto(heading) - } - } -} - diff --git a/Sources/MarkdownView/MarkdownView.swift b/Sources/MarkdownView/MarkdownView.swift index f111e4300..8f870d54e 100644 --- a/Sources/MarkdownView/MarkdownView.swift +++ b/Sources/MarkdownView/MarkdownView.swift @@ -1,52 +1,58 @@ import SwiftUI import Markdown -/// A view that displays read-only Markdown content. +/// A view that renders markdown content. public struct MarkdownView: View { - private var content: MarkdownContent + @ObservedObject private var content: MarkdownContent - @State private var viewSize = CGSize.zero - @Environment(\.colorScheme) private var colorScheme - @Environment(\.displayScale) private var displayScale - - @Environment(\.markdownViewStyle) private var markdownViewStyle @Environment(\.markdownFontGroup.body) private var bodyFont @Environment(\.markdownRendererConfiguration) private var configuration + /// Creates a view that renders given markdown string. + /// - Parameter text: The markdown source to render. public init(_ text: String) { - self.content = MarkdownContent( - raw: .plainText(text) - ) + self.content = MarkdownContent(text) } - @_spi(WIP) + /// Creates a view that renders the markdown from a local file at given url. + /// - Parameter url: The url to the markdown file to render. public init(_ url: URL) { - self.content = MarkdownContent( - raw: .url(url) - ) + self.content = MarkdownContent(url) } + /// Creates an instance that renders from a ``MarkdownContent`` . + /// - Parameter content: The ``MarkdownContent`` to render. public init(_ content: MarkdownContent) { self.content = content } public var body: some View { - markdownViewStyle - .makeBody( - configuration: MarkdownViewStyleConfiguration(body: _renderedBody) - ) - .erasedToAnyView() - .font(bodyFont) + _renderedBody.font(bodyFont) } @ViewBuilder private var _renderedBody: some View { - if configuration.math.shouldRender { - MathFirstMarkdownViewRenderer() - .makeBody(content: content, configuration: configuration) + if configuration.rendersMath { + MathFirstMarkdownViewRenderer().makeBody( + content: content, + configuration: configuration + ) } else { - CmarkFirstMarkdownViewRenderer() - .makeBody(content: content, configuration: configuration) + CmarkFirstMarkdownViewRenderer().makeBody( + content: content, + configuration: configuration + ) } } } + +@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) +#Preview(traits: .sizeThatFitsLayout) { + VStack { + MarkdownView("Hello **World**") + } + #if os(macOS) || os(iOS) + .textSelection(.enabled) + #endif + .padding() +} diff --git a/Sources/MarkdownView/Modifiers/BaseURLModifier.swift b/Sources/MarkdownView/Modifiers/BaseURLModifier.swift deleted file mode 100644 index 4c4657aaa..000000000 --- a/Sources/MarkdownView/Modifiers/BaseURLModifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// BaseURLModifier.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/9. -// - -import SwiftUI - -extension View { - nonisolated public func markdownBaseURL(_ url: URL) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.preferredBaseURL = url - } - } - - nonisolated public func markdownBaseURL(_ path: String) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.preferredBaseURL = URL(string: path) - } - } -} diff --git a/Sources/MarkdownView/Modifiers/BlockDirectiveRendererModifier.swift b/Sources/MarkdownView/Modifiers/BlockDirectiveRendererModifier.swift deleted file mode 100644 index a10030710..000000000 --- a/Sources/MarkdownView/Modifiers/BlockDirectiveRendererModifier.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// BlockDirectiveRendererModifier.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/9. -// - -import SwiftUI - -extension View { - /// Adds your custom block directive renderer. - /// - /// - parameter renderer: The renderer you have created to handle block directive rendering. - /// - parameter name: The name of the block directive. - nonisolated public func blockDirectiveRenderer( - _ renderer: some BlockDirectiveRenderer, - for name: String - ) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - BlockDirectiveRenderers.shared.addRenderer(renderer, for: name) - configuration.allowedBlockDirectiveRenderers.insert(name) - } - } -} diff --git a/Sources/MarkdownView/Modifiers/ListModifier.swift b/Sources/MarkdownView/Modifiers/ListModifier.swift deleted file mode 100644 index 00655e199..000000000 --- a/Sources/MarkdownView/Modifiers/ListModifier.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ListModifier.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/9. -// - -import SwiftUI - -extension View { - nonisolated public func markdownListIndent(_ indent: CGFloat) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.listConfiguration.leadingIndentation = indent - } - } - - nonisolated public func markdownUnorderedListMarker(_ marker: some UnorderedListMarkerProtocol) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.listConfiguration.unorderedListMarker = AnyUnorderedListMarkerProtocol(marker) - } - } - - nonisolated public func markdownOrderedListMarker(_ marker: some OrderedListMarkerProtocol) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.listConfiguration.orderedListMarker = AnyOrderedListMarkerProtocol(marker) - } - } - - nonisolated public func markdownComponentSpacing(_ spacing: CGFloat) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.componentSpacing = spacing - } - } -} diff --git a/Sources/MarkdownView/Modifiers/MarkdownImageRendererModifier.swift b/Sources/MarkdownView/Modifiers/MarkdownImageRendererModifier.swift deleted file mode 100644 index 2adaafc32..000000000 --- a/Sources/MarkdownView/Modifiers/MarkdownImageRendererModifier.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// MarkdownImageRendererModifier.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/9. -// - -import SwiftUI - -extension View { - /// Use custom renderer to render images. - /// - /// - parameter renderer: The render you created to handle image loading and rendering. - /// - parameter urlScheme: A scheme for deciding which renderer to use. - nonisolated public func markdownImageRenderer( - _ renderer: some MarkdownImageRenderer, - forURLScheme urlScheme: String - ) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - MarkdownImageRenders.shared.addRenderer(renderer, forURLScheme: urlScheme) - configuration.allowedImageRenderers.insert(urlScheme) - } - } -} diff --git a/Sources/MarkdownView/Modifiers/MathRenderingModifier.swift b/Sources/MarkdownView/Modifiers/MathRenderingModifier.swift deleted file mode 100644 index f5c5fd969..000000000 --- a/Sources/MarkdownView/Modifiers/MathRenderingModifier.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MathRenderingModifier.swift -// MarkdownView -// -// Created by LiYanan2004 on 2025/2/24. -// - -import SwiftUI - -extension View { - /// On macOS and iOS, parse and render math expression. - /// - /// - parameter enabled: A Boolean value that indicates whether to parse & render math expressions. The default value is true. - nonisolated public func markdownMathRenderingEnabled(_ enabled: Bool = true) -> some View { - transformEnvironment(\.markdownRendererConfiguration) { configuration in - configuration.math.shouldRender = enabled - if enabled { - configuration.allowedBlockDirectiveRenderers.insert("math") - BlockDirectiveRenderers.shared.addRenderer( - MathBlockDirectiveRenderer(), - for: "math" - ) - } - } - } -} diff --git a/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift b/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift index f31c83a47..d053d54c8 100644 --- a/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift +++ b/Sources/MarkdownView/Renderers/Cmark/CmarkFirstMarkdownViewRenderer.swift @@ -13,50 +13,12 @@ struct CmarkFirstMarkdownViewRenderer: MarkdownViewRenderer { content: MarkdownContent, configuration: MarkdownRendererConfiguration ) -> some View { - _makeAndCacheBody( - content: content, - configuration: configuration - ) - } - - private func _makeAndCacheBody( - content: MarkdownContent, - configuration: MarkdownRendererConfiguration - ) -> some View { - if let cached = CacheStorage.shared.withCacheIfAvailable( - content, - type: Cache.self - ), cached.configuration == configuration { - return AnyView(cached.renderedView) - } - var parseOptions = ParseOptions() if !configuration.allowedBlockDirectiveRenderers.isEmpty { parseOptions.insert(.parseBlockDirectives) } - let renderedView = CmarkNodeVisitor(configuration: configuration) - .makeBody(for: content.parse(options: parseOptions)) - .erasedToAnyView() - - CacheStorage.shared.addCache( - Cache( - markdownContent: content, - configuration: configuration, - renderedView: renderedView - ) - ) - - return renderedView - } -} - -extension CmarkFirstMarkdownViewRenderer { - struct Cache: Cacheable { - var markdownContent: MarkdownContent - var configuration: MarkdownRendererConfiguration - var renderedView: any View - - var cacheKey: some Hashable { markdownContent } + return CmarkNodeVisitor(configuration: configuration) + .makeBody(for: content.document(options: parseOptions)) } } diff --git a/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift b/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift index 5acb7eca7..39dfd77ee 100644 --- a/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift +++ b/Sources/MarkdownView/Renderers/Cmark/CmarkNodeVisitor.swift @@ -47,7 +47,7 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { } func visitText(_ text: Markdown.Text) -> MarkdownNodeView { - if configuration.math.shouldRender { + if configuration.rendersMath { InlineMathOrText(text: text.plainText) .makeBody(configuration: configuration) } else { @@ -187,7 +187,7 @@ struct CmarkNodeVisitor: @preconcurrency MarkupVisitor { func visitHeading(_ heading: Heading) -> MarkdownNodeView { MarkdownNodeView { - MarkdownHeading(heading: heading) + HeadingText(heading: heading) } } diff --git a/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift b/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift new file mode 100644 index 000000000..9d642088e --- /dev/null +++ b/Sources/MarkdownView/Renderers/MarkdownRendererConfiguration.swift @@ -0,0 +1,68 @@ +// +// MarkdownRendererConfiguration.swift +// MarkdownView +// +// Created by LiYanan2004 on 2024/12/11. +// + +import Foundation +import SwiftUI + +struct MarkdownRendererConfiguration: Equatable, KeyPathModifiable, Sendable { + var preferredBaseURL: URL? + var componentSpacing: CGFloat = 8 + + var math = MathRendering() + var rendersMath: Bool { math.isEnabled } + + var linkTintColor: Color = .accentColor + var inlineCodeTintColor: Color = .accentColor + var blockQuoteTintColor: Color = .accentColor + + var list = MarkdownListConfiguration() + + var allowedImageRenderers: Set = ["https", "http"] + var allowedBlockDirectiveRenderers: Set = [] +} + +// MARK: - List + +extension MarkdownRendererConfiguration { + struct MarkdownListConfiguration: Hashable, @unchecked Sendable { + var leadingIndentation: CGFloat = 12 + var unorderedListMarker = AnyUnorderedListMarkerProtocol(.bullet) + var orderedListMarker = AnyOrderedListMarkerProtocol(.increasingDigits) + } +} + +// MARK: - Math Rendering + +extension MarkdownRendererConfiguration { + struct MathRendering: Sendable, Hashable { + var isEnabled: Bool = false + var displayMathStorage: [UUID : String] = [:] + + mutating func setNeedsRendering(_ needRenderMath: Bool) { + isEnabled = needRenderMath + } + + mutating func appendDisplayMath(_ displayMath: some StringProtocol) -> UUID { + let id = UUID() + displayMathStorage[id] = String(displayMath) + return id + } + } +} + +// MARK: - SwiftUI Environment + +struct MarkdownRendererConfigurationKey: EnvironmentKey { + static let defaultValue: MarkdownRendererConfiguration = .init() +} + +extension EnvironmentValues { + var markdownRendererConfiguration: MarkdownRendererConfiguration { + get { self[MarkdownRendererConfigurationKey.self] } + set { self[MarkdownRendererConfigurationKey.self] = newValue } + } +} diff --git a/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift b/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift index b8c508bf3..16a2a9e3c 100644 --- a/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift +++ b/Sources/MarkdownView/Renderers/MarkdownViewRenderer.swift @@ -21,3 +21,15 @@ protocol MarkdownViewRenderer { configuration: MarkdownRendererConfiguration ) -> Body } + +extension MarkdownViewRenderer { + internal func parseOptions(for configuration: MarkdownRendererConfiguration) -> ParseOptions { + var options = ParseOptions() + + if !configuration.allowedBlockDirectiveRenderers.isEmpty { + options.insert(.parseBlockDirectives) + } + + return options + } +} diff --git a/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.ParsingRangesExtractor.swift b/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.ParsingRangesExtractor.swift deleted file mode 100644 index 102faafec..000000000 --- a/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.ParsingRangesExtractor.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// MathFirstMarkdownViewRenderer.ParsingRangesExtractor.swift -// MarkdownView -// -// Created by Yanan Li on 2025/4/17. -// - -import Markdown - -extension MathFirstMarkdownViewRenderer { - struct ParsingRangesExtractor: MarkupWalker { - private var excludedRanges: [Range] = [] - - func parsableRanges(in text: String) -> [Range] { - var allowedRanges: [Range] = [] - let excludedRanges = self.excludedRanges.map { - ($0.lowerBound.index(in: text)..<$0.upperBound.index(in: text)) - } - - let fullRange = text.startIndex.. String.Index { - var idx = string.startIndex - var currentLine = 1 - while currentLine < self.line && idx < string.endIndex { - if string[idx] == "\n" { - currentLine += 1 - } - idx = string.index(after: idx) - } - guard let utf8LineStart = idx.samePosition(in: string.utf8) else { - return string.endIndex - } - let byteOffset = self.column - 1 - let targetUtf8Index = string.utf8.index(utf8LineStart, offsetBy: byteOffset, limitedBy: string.utf8.endIndex) ?? string.utf8.endIndex - return targetUtf8Index.samePosition(in: string) ?? string.endIndex - } -} diff --git a/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift b/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift index b79db83de..c4269174d 100644 --- a/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift +++ b/Sources/MarkdownView/Renderers/Math/MathFirstMarkdownViewRenderer.swift @@ -14,10 +14,10 @@ struct MathFirstMarkdownViewRenderer: MarkdownViewRenderer { configuration: MarkdownRendererConfiguration ) -> some View { var configuration = configuration - var rawText = content.raw.text + var rawText = (try? content.markdown) ?? "" var extractor = ParsingRangesExtractor() - extractor.visit(content.parse(options: ParseOptions().union(.parseBlockDirectives))) + extractor.visit(content.document()) for range in extractor.parsableRanges(in: rawText) { let segment = rawText[range] let segmentParser = MathParser(text: segment) @@ -32,8 +32,67 @@ struct MathFirstMarkdownViewRenderer: MarkdownViewRenderer { } } - let _content = MarkdownContent(raw: .plainText(rawText)) - return CmarkFirstMarkdownViewRenderer() - .makeBody(content: _content, configuration: configuration) + return CmarkFirstMarkdownViewRenderer().makeBody( + content: MarkdownContent(.plainText(rawText)), + configuration: configuration + ) + } +} + +// MARK: - Auxiliary + +fileprivate extension MathFirstMarkdownViewRenderer { + struct ParsingRangesExtractor: MarkupWalker { + private var excludedRanges: [Range] = [] + + func parsableRanges(in text: String) -> [Range] { + var allowedRanges: [Range] = [] + let excludedRanges = self.excludedRanges.map { + ($0.lowerBound.index(in: text)..<$0.upperBound.index(in: text)) + } + + let fullRange = text.startIndex.. String.Index { + var idx = string.startIndex + var currentLine = 1 + while currentLine < self.line && idx < string.endIndex { + if string[idx] == "\n" { + currentLine += 1 + } + idx = string.index(after: idx) + } + guard let utf8LineStart = idx.samePosition(in: string.utf8) else { + return string.endIndex + } + let byteOffset = self.column - 1 + let targetUtf8Index = string.utf8.index(utf8LineStart, offsetBy: byteOffset, limitedBy: string.utf8.endIndex) ?? string.utf8.endIndex + return targetUtf8Index.samePosition(in: string) ?? string.endIndex } } diff --git a/Sources/MarkdownView/Renderers/Math/MathParser.swift b/Sources/MarkdownView/Renderers/Math/MathParser.swift index 2d2afc9f8..687d5a081 100644 --- a/Sources/MarkdownView/Renderers/Math/MathParser.swift +++ b/Sources/MarkdownView/Renderers/Math/MathParser.swift @@ -3,6 +3,7 @@ // MarkdownView // // Created by LiYanan2004 on 2025/2/24. +// Credits to colinc86/LaTeXSwiftUI // import SwiftUI @@ -11,9 +12,6 @@ import LaTeXSwiftUI import MathJaxSwift #endif -/* - Credits to colinc86/LaTeXSwiftUI - */ @_spi(MarkdownMath) public struct MathParser { public var text: any StringProtocol diff --git a/Sources/MarkdownView/Renderers/Node Representations/Block Directives/Renderers/MathBlockDirectiveRenderer.swift b/Sources/MarkdownView/Renderers/Node Representations/Block Directives/Renderers/MathBlockDirectiveRenderer.swift index 0c9576155..5c66d1e9b 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/Block Directives/Renderers/MathBlockDirectiveRenderer.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/Block Directives/Renderers/MathBlockDirectiveRenderer.swift @@ -26,7 +26,7 @@ fileprivate struct DisplayMath: View { @Environment(\.markdownFontGroup.displayMath) private var font @Environment(\.markdownRendererConfiguration.math) private var math private var latexMath: String? { - math.displayMathStorage?[mathIdentifier] + math.displayMathStorage[mathIdentifier] } var body: some View { diff --git a/Sources/MarkdownView/Renderers/Node Representations/Heading/MarkdownHeading.swift b/Sources/MarkdownView/Renderers/Node Representations/HeadingText.swift similarity index 96% rename from Sources/MarkdownView/Renderers/Node Representations/Heading/MarkdownHeading.swift rename to Sources/MarkdownView/Renderers/Node Representations/HeadingText.swift index e3a8a8dd4..4791b4b21 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/Heading/MarkdownHeading.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/HeadingText.swift @@ -1,5 +1,5 @@ // -// MarkdownHeading.swift +// HeadingText.swift // MarkdownView // // Created by Yanan Li on 2025/2/22. @@ -8,7 +8,7 @@ import SwiftUI import Markdown -struct MarkdownHeading: View { +struct HeadingText: View { let heading: Heading @Environment(\.markdownRendererConfiguration) private var configuration diff --git a/Sources/MarkdownView/Renderers/Node Representations/Images/Protocol/MarkdownImageRenderer.swift b/Sources/MarkdownView/Renderers/Node Representations/Images/Protocol/MarkdownImageRenderer.swift index 764e6d6ae..5a58237f6 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/Images/Protocol/MarkdownImageRenderer.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/Images/Protocol/MarkdownImageRenderer.swift @@ -7,11 +7,17 @@ import SwiftUI -/// A type that renders images. +/// A type that renders Markdown image nodes for a specific URL scheme. /// -/// Think of this type as a SwiftUI View wrapper. +/// The protocol mirrors SwiftUI’s `View` construction model: implement +/// ``MarkdownImageRenderer/makeBody(configuration:)`` and return a view hierarchy +/// that knows how to fetch and display the requested image. The method is +/// invoked on the main actor, so heavy work (networking, decoding, etc.) should +/// be delegated to another view or task. /// -/// Don't directly access view dependencies (e.g. `@Environment`), use a separate view instead. +/// > Tip: Because protocol witnesses cannot use property wrappers, keep the +/// > renderer itself lightweight and move any `@Environment` or `@State` +/// > dependencies into a nested SwiftUI view. @preconcurrency @MainActor public protocol MarkdownImageRenderer { @@ -29,17 +35,24 @@ public protocol MarkdownImageRenderer { typealias Configuration = MarkdownImageRendererConfiguration } -/// The properties of a markdown image. +/// The immutable properties of a Markdown image node. public struct MarkdownImageRendererConfiguration: Sendable { /// The source url of an image. + /// + /// When the original Markdown uses a relative path and a base URL was + /// provided via ``View/markdownBaseURL(_:)``, this value already contains the + /// resolved absolute URL. Otherwise it is the URL verbatim from the Markdown. public var url: URL /// The alternative text of an image. + /// + /// MarkdownView automatically suppresses the alternative text when the image + /// appears inside a link so you can decide how to expose descriptive text. public var alternativeText: String? } // MARK: - Type Erasure -/// A type-erasure for type conforms to `MarkdownImageRenderer`. +/// A type-erased wrapper for any ``MarkdownImageRenderer`` implementation. public struct AnyMarkdownImageRenderer: MarkdownImageRenderer { public typealias Body = AnyView diff --git a/Sources/MarkdownView/Renderers/Node Representations/Block Quotes/MarkdownBlockQuote.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownBlockQuote.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Block Quotes/MarkdownBlockQuote.swift rename to Sources/MarkdownView/Renderers/Node Representations/MarkdownBlockQuote.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift index fb3854a14..022b4527f 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/MarkdownList.swift @@ -7,9 +7,9 @@ struct MarkdownList: View { @Environment(\.markdownRendererConfiguration) private var configuration private var marker: Either { if listItemsContainer is UnorderedList { - return .left(configuration.listConfiguration.unorderedListMarker) + return .left(configuration.list.unorderedListMarker) } else if listItemsContainer is OrderedList { - return .right(configuration.listConfiguration.orderedListMarker) + return .right(configuration.list.orderedListMarker) } else { fatalError("Marker Protocol not implemented for \(type(of: listItemsContainer)).") } @@ -26,7 +26,7 @@ struct MarkdownList: View { ) { (index, listItem) in HStack(alignment: .firstTextBaseline) { CheckboxOrMarker(list: self, listItem: listItem, index: index) - .padding(.leading, depth == 0 ? configuration.listConfiguration.leadingIndentation : 0) + .padding(.leading, depth == 0 ? configuration.list.leadingIndentation : 0) CmarkNodeVisitor(configuration: configuration) .makeBody(for: listItem) } diff --git a/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift index fd9c23539..9eb6b5744 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/MarkdownNodeView.swift @@ -37,7 +37,7 @@ struct MarkdownNodeView: View { var body: some View { Group { if case .left(let attributedString) = storage { - _MarkdownText(attributedString) + MarkdownText(attributedString) } else if case .right(let view) = storage { view } diff --git a/Sources/MarkdownView/Renderers/Node Representations/Code Blocks/MarkdownStyledCodeBlock.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownStyledCodeBlock.swift similarity index 100% rename from Sources/MarkdownView/Renderers/Node Representations/Code Blocks/MarkdownStyledCodeBlock.swift rename to Sources/MarkdownView/Renderers/Node Representations/MarkdownStyledCodeBlock.swift diff --git a/Sources/MarkdownView/Renderers/Node Representations/_MarkdownText.swift b/Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift similarity index 96% rename from Sources/MarkdownView/Renderers/Node Representations/_MarkdownText.swift rename to Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift index 3f59e760f..f69bd362f 100644 --- a/Sources/MarkdownView/Renderers/Node Representations/_MarkdownText.swift +++ b/Sources/MarkdownView/Renderers/Node Representations/MarkdownText.swift @@ -1,5 +1,5 @@ // -// _MarkdownText.swift +// MarkdownText.swift // MarkdownView // // Created by Yanan Li on 2025/10/20. @@ -10,7 +10,7 @@ import SwiftUI /// A view that displays parsed HTML asynchronously. /// /// Convert HTML into `AttributedString` asynchronously to avoid `AttributeGraph` crash. -struct _MarkdownText: View { +struct MarkdownText: View { var text: AttributedString @State private var attributedString: AttributedString? diff --git a/Sources/MarkdownView/Table of Content/MarkdownHeading.swift b/Sources/MarkdownView/Table of Content/MarkdownHeading.swift new file mode 100644 index 000000000..9701e6140 --- /dev/null +++ b/Sources/MarkdownView/Table of Content/MarkdownHeading.swift @@ -0,0 +1,45 @@ +// +// MarkdownHeading.swift +// MarkdownView +// +// Created by Yanan Li on 2025/10/28. +// + +import Foundation +import Markdown + +/// A representation of a markdown heading. +public struct MarkdownHeading: Hashable, Sendable { + private var heading: Markdown.Heading + + /// Heading level, starting from 1. + public var level: Int { + heading.level + } + /// The range of the heading in the raw Markdown. + /// + /// The range originates from `swift-markdown`’s parsing result. It is + /// present when the Markdown source carried location information (for + /// example when it was loaded from a file URL). + public var range: SourceRange? { + heading.range + } + /// The content text of the heading. + public var plainText: String { + heading.plainText + } + + init(heading: Heading) { + self.heading = heading + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(level) + hasher.combine(range) + hasher.combine(plainText) + } + + public static func == (lhs: MarkdownHeading, rhs: MarkdownHeading) -> Bool { + lhs.heading.isIdentical(to: rhs.heading) + } +} diff --git a/Sources/MarkdownView/Table of Content/MarkdownTableOfContent.swift b/Sources/MarkdownView/Table of Content/MarkdownTableOfContent.swift new file mode 100644 index 000000000..751d7ce9c --- /dev/null +++ b/Sources/MarkdownView/Table of Content/MarkdownTableOfContent.swift @@ -0,0 +1,61 @@ +import SwiftUI +import Markdown + +/// A view that produces content from the headings found in a Markdown document. +/// +/// Pass the same ``MarkdownContent`` that drives your ``MarkdownView`` so the +/// table of contents stays in sync. The easiest way to do this is to wrap both +/// views in a ``MarkdownReader``. +public struct MarkdownTableOfContent: View { + @ObservedObject private var content: MarkdownContent + private var contents: (_ headings: [MarkdownHeading]) -> Content + + public init( + _ content: MarkdownContent, + @ViewBuilder contents: @escaping ([MarkdownHeading]) -> Content + ) { + self.content = content + self.contents = contents + } + + @_disfavoredOverload + public init( + _ content: URL, + @ViewBuilder contents: @escaping ([MarkdownHeading]) -> Content + ) { + self.content = .init(content) + self.contents = contents + } + + @_disfavoredOverload + public init( + _ content: String, + @ViewBuilder contents: @escaping ([MarkdownHeading]) -> Content + ) { + self.content = .init(content) + self.contents = contents + } + + private var headings: [MarkdownHeading] { + var toc = TableOfContentVisitor() + toc.visit( + content.store.documents.first ?? content.document() + ) + return toc.headings + } + + public var body: some View { + contents(headings) + } +} + +// MARK: - Auxiliary + +fileprivate struct TableOfContentVisitor: MarkupWalker { + private(set) var headings: [MarkdownHeading] = [] + + mutating func visitHeading(_ heading: Markdown.Heading) { + headings.append(MarkdownHeading(heading: heading)) + descendInto(heading) + } +} diff --git a/Sources/MarkdownView/View Modifiers/BaseURLModifier.swift b/Sources/MarkdownView/View Modifiers/BaseURLModifier.swift new file mode 100644 index 000000000..9807a932b --- /dev/null +++ b/Sources/MarkdownView/View Modifiers/BaseURLModifier.swift @@ -0,0 +1,44 @@ +// +// BaseURLModifier.swift +// MarkdownView +// +// Created by Yanan Li on 2025/2/9. +// + +import SwiftUI + +extension View { + /// Sets the base URL used to resolve relative image paths inside Markdown. + /// + /// Markdown image elements that omit a scheme (for example `images/logo.png`) + /// are only displayable once they are resolved against a base URL. Use this + /// modifier whenever your Markdown references local documentation assets or + /// CDN paths. + /// + /// ```swift + /// MarkdownView(markdown) + /// .markdownBaseURL(Bundle.main.bundleURL) + /// // Markdown: ![Diagram](Resources/diagram.svg) + /// ``` + /// + /// - Parameter url: The base location that relative URLs are resolved + /// against. Only the scheme/host/path are used; query parameters are + /// preserved when the Markdown specifies them. + nonisolated public func markdownBaseURL(_ url: URL) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.preferredBaseURL = url + } + } + + /// Convenience overload that creates the base URL from a string. + /// + /// If the string cannot be converted into a valid `URL`, the modifier is a + /// no-op. + /// + /// - Parameter path: The raw string representation of the base URL. + nonisolated public func markdownBaseURL(_ path: String) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.preferredBaseURL = URL(string: path) + } + } +} diff --git a/Sources/MarkdownView/View Modifiers/Custom Renderers/BlockDirectiveRendererModifier.swift b/Sources/MarkdownView/View Modifiers/Custom Renderers/BlockDirectiveRendererModifier.swift new file mode 100644 index 000000000..bac1b4e7b --- /dev/null +++ b/Sources/MarkdownView/View Modifiers/Custom Renderers/BlockDirectiveRendererModifier.swift @@ -0,0 +1,49 @@ +// +// BlockDirectiveRendererModifier.swift +// MarkdownView +// +// Created by Yanan Li on 2025/2/9. +// + +import SwiftUI + +extension View { + /// Registers a custom renderer for a block directive name. + /// + /// Block directives (`::name{}`) are only rendered when a matching renderer + /// exists and the name is present in the allow list. This modifier performs + /// both tasks for you: it stores the renderer in the shared registry (the + /// last registration wins for the same name, comparisons are + /// case-insensitive) and it inserts the name into the environment’s + /// allow list. Directives without a renderer fall back to rendering their + /// body with the default Markdown visitor. + /// + /// ```swift + /// struct CalloutDirective: BlockDirectiveRenderer { + /// func makeBody(configuration: Configuration) -> some View { + /// Label(configuration.arguments.first?.value ?? "Info", + /// systemImage: "info.circle") + /// .padding() + /// .background(RoundedRectangle(cornerRadius: 8).fill(.blue.opacity(0.1))) + /// } + /// } + /// + /// MarkdownView(markdown) + /// .blockDirectiveRenderer(CalloutDirective(), for: "callout") + /// // Markdown: ::callout[level:warning]{ ... } + /// ``` + /// + /// - Parameters: + /// - renderer: The renderer responsible for producing the SwiftUI view. + /// - name: The directive name to match. Use lowercase names to avoid + /// collisions with existing renderers. + nonisolated public func blockDirectiveRenderer( + _ renderer: some BlockDirectiveRenderer, + for name: String + ) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + BlockDirectiveRenderers.shared.addRenderer(renderer, for: name) + configuration.allowedBlockDirectiveRenderers.insert(name) + } + } +} diff --git a/Sources/MarkdownView/View Modifiers/Custom Renderers/ImageRendererModifier.swift b/Sources/MarkdownView/View Modifiers/Custom Renderers/ImageRendererModifier.swift new file mode 100644 index 000000000..35ab2e17d --- /dev/null +++ b/Sources/MarkdownView/View Modifiers/Custom Renderers/ImageRendererModifier.swift @@ -0,0 +1,54 @@ +// +// ImageRendererModifier.swift +// MarkdownView +// +// Created by Yanan Li on 2025/2/9. +// + +import SwiftUI + +extension View { + /// Registers a custom renderer for Markdown images that use the given URL scheme. + /// + /// Markdown image nodes choose their renderer by looking at the `scheme` + /// portion of the image URL. By default the built-in HTTP(S) renderer is + /// available. Call this modifier to support additional schemes (for example + /// `asset://` to load bundle resources or `ipfs://` to talk to a custom + /// client). + /// + /// The registration performs two actions: + /// 1. It stores the renderer in a shared registry (the most recently + /// registered renderer wins for a given scheme). + /// 2. It inserts the scheme into the environment’s allow list so the + /// renderer is considered during view construction. If an image uses a + /// scheme that is not on the allow list, MarkdownView intentionally falls + /// back to ``View/markdownBaseURL(_:)`` or to plain text for safety. + /// + /// ```swift + /// struct AssetImageRenderer: MarkdownImageRenderer { + /// func makeBody(configuration: Configuration) -> some View { + /// Image(configuration.url.lastPathComponent) + /// .resizable() + /// .scaledToFit() + /// } + /// } + /// + /// MarkdownView(markdown) + /// .markdownImageRenderer(AssetImageRenderer(), forURLScheme: "asset") + /// // Markdown: ![Logo](asset://logo.png) + /// ``` + /// + /// - Parameters: + /// - renderer: Your renderer type that knows how to load and display the image. + /// - urlScheme: The scheme to match (case-insensitive). Use unique schemes + /// to avoid clobbering system renderers. + nonisolated public func markdownImageRenderer( + _ renderer: some MarkdownImageRenderer, + forURLScheme urlScheme: String + ) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + MarkdownImageRenders.shared.addRenderer(renderer, forURLScheme: urlScheme) + configuration.allowedImageRenderers.insert(urlScheme) + } + } +} diff --git a/Sources/MarkdownView/Modifiers/Heading/HeadingPaddingModifier.swift b/Sources/MarkdownView/View Modifiers/Heading/HeadingPaddingModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/Heading/HeadingPaddingModifier.swift rename to Sources/MarkdownView/View Modifiers/Heading/HeadingPaddingModifier.swift diff --git a/Sources/MarkdownView/Modifiers/Heading/HeadingStyleModifier.swift b/Sources/MarkdownView/View Modifiers/Heading/HeadingStyleModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/Heading/HeadingStyleModifier.swift rename to Sources/MarkdownView/View Modifiers/Heading/HeadingStyleModifier.swift diff --git a/Sources/MarkdownView/View Modifiers/MathRenderingModifier.swift b/Sources/MarkdownView/View Modifiers/MathRenderingModifier.swift new file mode 100644 index 000000000..5b6872141 --- /dev/null +++ b/Sources/MarkdownView/View Modifiers/MathRenderingModifier.swift @@ -0,0 +1,39 @@ +// +// MathRenderingModifier.swift +// MarkdownView +// +// Created by LiYanan2004 on 2025/2/24. +// + +import SwiftUI + +extension View { + /// Opts the surrounding Markdown views into parsing and rendering math expressions. + /// + /// Math support is disabled by default so plain Markdown renders quickly. + /// Calling this modifier rewrites display math blocks (`$$ ... $$`) into a + /// block-directive placeholder that is then rendered through LaTeXSwiftUI on + /// iOS and macOS. On other platforms the directive safely degrades to an + /// empty view. + /// + /// ```swift + /// MarkdownView(markdown) + /// .markdownMathRenderingEnabled() + /// ``` + /// + /// - Parameter enabled: Set to `false` to temporarily suppress math parsing + /// for a subtree. The default is `true`, which turns math rendering on for + /// the given view hierarchy. + nonisolated public func markdownMathRenderingEnabled(_ enabled: Bool = true) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.math.isEnabled = enabled + if enabled { + configuration.allowedBlockDirectiveRenderers.insert("math") + BlockDirectiveRenderers.shared.addRenderer( + MathBlockDirectiveRenderer(), + for: "math" + ) + } + } + } +} diff --git a/Sources/MarkdownView/Modifiers/BlockQuoteStyleModifier.swift b/Sources/MarkdownView/View Modifiers/Styling/BlockQuoteStyleModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/BlockQuoteStyleModifier.swift rename to Sources/MarkdownView/View Modifiers/Styling/BlockQuoteStyleModifier.swift diff --git a/Sources/MarkdownView/Modifiers/CodeBlockModifier.swift b/Sources/MarkdownView/View Modifiers/Styling/CodeBlockModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/CodeBlockModifier.swift rename to Sources/MarkdownView/View Modifiers/Styling/CodeBlockModifier.swift diff --git a/Sources/MarkdownView/View Modifiers/Styling/ListModifier.swift b/Sources/MarkdownView/View Modifiers/Styling/ListModifier.swift new file mode 100644 index 000000000..ab189dc6a --- /dev/null +++ b/Sources/MarkdownView/View Modifiers/Styling/ListModifier.swift @@ -0,0 +1,56 @@ +// +// ListModifier.swift +// MarkdownView +// +// Created by Yanan Li on 2025/2/9. +// + +import SwiftUI + +extension View { + /// Adjusts the leading indentation applied to list markers. + /// + /// The value applies to both ordered and unordered lists rendered by + /// `MarkdownView`. + /// + /// - Parameter indent: The padding, in points, to apply in front of list content. + nonisolated public func markdownListIndent(_ indent: CGFloat) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.list.leadingIndentation = indent + } + } + + /// Replaces the marker that unordered lists use for each item. + /// + /// Provide a type that conforms to ``UnorderedListMarkerProtocol`` to drive + /// the bullet’s appearance. + nonisolated public func markdownUnorderedListMarker( + _ marker: some UnorderedListMarkerProtocol + ) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.list.unorderedListMarker = AnyUnorderedListMarkerProtocol(marker) + } + } + + /// Replaces the marker that ordered lists use for each row. + /// + /// Provide a type that conforms to ``OrderedListMarkerProtocol`` to control + /// numbering, prefixes, and suffixes. + nonisolated public func markdownOrderedListMarker( + _ marker: some OrderedListMarkerProtocol + ) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.list.orderedListMarker = AnyOrderedListMarkerProtocol(marker) + } + } + + /// Sets the vertical spacing between block-level Markdown components such as + /// paragraphs, list items, and block quotes. + /// + /// - Parameter spacing: The spacing value passed to `VStack` containers inside MarkdownView. + nonisolated public func markdownComponentSpacing(_ spacing: CGFloat) -> some View { + transformEnvironment(\.markdownRendererConfiguration) { configuration in + configuration.componentSpacing = spacing + } + } +} diff --git a/Sources/MarkdownView/Modifiers/TintColorModifier.swift b/Sources/MarkdownView/View Modifiers/Styling/TintColorModifier.swift similarity index 94% rename from Sources/MarkdownView/Modifiers/TintColorModifier.swift rename to Sources/MarkdownView/View Modifiers/Styling/TintColorModifier.swift index ea9e09614..a95212d28 100644 --- a/Sources/MarkdownView/Modifiers/TintColorModifier.swift +++ b/Sources/MarkdownView/View Modifiers/Styling/TintColorModifier.swift @@ -31,7 +31,7 @@ extension View { /// Components that can apply a tint color. @_documentation(visibility: internal) -public enum TintableComponent { +public enum TintableComponent: Hashable, Sendable { case blockQuote case inlineCodeBlock case link diff --git a/Sources/MarkdownView/Modifiers/Table/MarkdownTableCellOverlayModifier.swift b/Sources/MarkdownView/View Modifiers/Table/MarkdownTableCellOverlayModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/Table/MarkdownTableCellOverlayModifier.swift rename to Sources/MarkdownView/View Modifiers/Table/MarkdownTableCellOverlayModifier.swift diff --git a/Sources/MarkdownView/Modifiers/Table/MarkdownTableCellPaddingModifier.swift b/Sources/MarkdownView/View Modifiers/Table/MarkdownTableCellPaddingModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/Table/MarkdownTableCellPaddingModifier.swift rename to Sources/MarkdownView/View Modifiers/Table/MarkdownTableCellPaddingModifier.swift diff --git a/Sources/MarkdownView/Modifiers/Table/MarkdownTableRowBackgroundStyleModifier.swift b/Sources/MarkdownView/View Modifiers/Table/MarkdownTableRowBackgroundStyleModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/Table/MarkdownTableRowBackgroundStyleModifier.swift rename to Sources/MarkdownView/View Modifiers/Table/MarkdownTableRowBackgroundStyleModifier.swift diff --git a/Sources/MarkdownView/Modifiers/Table/TableStyleModifier.swift b/Sources/MarkdownView/View Modifiers/Table/TableStyleModifier.swift similarity index 100% rename from Sources/MarkdownView/Modifiers/Table/TableStyleModifier.swift rename to Sources/MarkdownView/View Modifiers/Table/TableStyleModifier.swift diff --git a/Sources/MarkdownView/Modifiers/FontModifier.swift b/Sources/MarkdownView/View Modifiers/Text Formatting/FontModifier.swift similarity index 99% rename from Sources/MarkdownView/Modifiers/FontModifier.swift rename to Sources/MarkdownView/View Modifiers/Text Formatting/FontModifier.swift index ceff5af4b..6339ba1dd 100644 --- a/Sources/MarkdownView/Modifiers/FontModifier.swift +++ b/Sources/MarkdownView/View Modifiers/Text Formatting/FontModifier.swift @@ -8,7 +8,6 @@ import SwiftUI extension View { - /// Apply a font group to MarkdownView. /// /// Customize fonts for multiple types of text. @@ -41,5 +40,4 @@ extension View { } } } - } diff --git a/Sources/MarkdownView/WIP/MarkdownText.swift b/Sources/MarkdownView/WIP/MarkdownText.swift deleted file mode 100644 index d30d85437..000000000 --- a/Sources/MarkdownView/WIP/MarkdownText.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// MarkdownText.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -internal struct MarkdownText: View { - private var content: MarkdownContent - @Environment(\.colorScheme) private var colorScheme - @Environment(\.displayScale) private var displayScale - - @Environment(\.self) private var environment - - public init(_ text: String) { - content = MarkdownContent(raw: .plainText(text)) - } - - @_spi(WIP) - public init(_ url: URL) { - content = MarkdownContent(raw: .url(url)) - } - - public var body: some View { - MarkdownTextRenderer(environment: environment) - .renderMarkdownContent(content) - .render() - - // TODO: Loading Image async and replace placeholder node. - /* - .onChange(of: content, initial: true) { - Task.detached { - var documentNode = MarkdownTextRenderer - .walkDocument(content.document) - await documentNode.modifyOverIteration { node in - guard node.kind == .placeholder, - case let .task(task) = node.content else { - return - } - - if let result = try? await task.value, let image = result as? Image { - node.kind = .image - node.content = .image(image) - } - } - await MainActor.run { - self.documentNodes = documentNode - } - } - } - */ - } -} - -// MARK: - Preview - -#Preview { - let markdown = #""" - ## Apple - - Here is the [Apple](https://www.apple.com) *website*. - - ### SwiftUI - - `SwiftUI` is Apple's **declaritive**, **cross-platform** UI framework. - - Here is a basic example, it shows: - - how to create a simple view - - The body is an opaque type of `View` - - ```swift - import SwiftUI - - struct ContentView: View { - var body: some View { - EmptyView() - } - } - ``` - """# - MarkdownText(markdown) - .textSelectionEnabledIfPossible() - .padding() - .lineSpacing(8) -} - -fileprivate extension View { - @ViewBuilder - func textSelectionEnabledIfPossible(_ enabled: Bool = true) -> some View { - #if os(macOS) || os(iOS) - if #available(iOS 15.0, macOS 12.0, *), enabled { - textSelection(.enabled) - } else { - self - } - #else - self - #endif - } -} diff --git a/Sources/MarkdownView/WIP/MarkdownTextKind.swift b/Sources/MarkdownView/WIP/MarkdownTextKind.swift deleted file mode 100644 index 8eca810fd..000000000 --- a/Sources/MarkdownView/WIP/MarkdownTextKind.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// MarkdownTextKind.swift -// MarkdownView -// -// Created by LiYanan2004 on 2025/4/1. -// - -import Foundation - -enum MarkdownTextKind: Sendable, Hashable { - case document - - case paragraph - case heading - case plainText - case strikethrough - case boldText - case italicText - case link - - case softBreak - case hardBreak - - case code - case codeBlock - - case orderedList - case unorderedList - case listItem - - case placeholder - case image - - case unknown -} diff --git a/Sources/MarkdownView/WIP/MarkdownTextNode.swift b/Sources/MarkdownView/WIP/MarkdownTextNode.swift deleted file mode 100644 index 494b6f9df..000000000 --- a/Sources/MarkdownView/WIP/MarkdownTextNode.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// MarkdownTextNode.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/11. -// - -import Foundation -import SwiftUI - -@MainActor -@preconcurrency -struct MarkdownTextNode: Sendable, AllowingModifyThroughKeyPath { - var kind: MarkdownTextKind - var children: [MarkdownTextNode] - var content: Content? - var index: Int? - var depth: Int? - var environment: EnvironmentValues - - enum Content: Sendable { - case text(String) - case heading(Int) - case codeLanguage(String) - case link(String, URL) - case image(Image) - case task(Task) - } - - mutating func modifyOverIteration(_ body: (inout Self) async throws -> Void) async rethrows { - try await body(&self) - for index in children.indices { - try await children[index].modifyOverIteration(body) - } - } -} - -extension MarkdownTextNode { - func render() -> Text { - switch kind { - case .document: - return children - .map { $0.render() } - .reduce(Text(""), +) - case .plainText: - guard case let .text(text) = content! else { - fatalError("Unsupported content for .plainText") - } - return Text(text) - case .hardBreak: - return BreakTextRenderer() - .body( - context: BreakTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .softBreak: - return BreakTextRenderer() - .body( - context: BreakTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .placeholder: - return Text(" ") - case .paragraph: - return ParagraphTextRenderer() - .body( - context: HeadingTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .heading: - return HeadingTextRenderer() - .body( - context: HeadingTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .italicText, .boldText, .strikethrough: - return FormattedTextRenderer() - .body( - context: FormattedTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .link: - return LinkTextRenderer() - .body( - context: LinkTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .image: - return ImageTextRenderer() - .body( - context: ImageTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .codeBlock: - return CodeBlockTextRenderer() - .body( - context: CodeBlockTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .code: - return InlineCodeTextRenderer() - .body( - context: InlineCodeTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .orderedList: - return OrderedListTextRenderer() - .body( - context: OrderedListTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .unorderedList: - return UnorderedListTextRenderer() - .body( - context: UnorderedListTextRenderer.Context( - node: self, - environment: environment - ) - ) - case .listItem: - return ListItemTextRenderer() - .body( - context: ListItemTextRenderer.Context( - node: self, - environment: environment - ) - ) - default: - return Text("Unimplemented: \(kind)") - } - } -} diff --git a/Sources/MarkdownView/WIP/MarkdownTextRenderer.Visit.swift b/Sources/MarkdownView/WIP/MarkdownTextRenderer.Visit.swift deleted file mode 100644 index ed24e77d1..000000000 --- a/Sources/MarkdownView/WIP/MarkdownTextRenderer.Visit.swift +++ /dev/null @@ -1,247 +0,0 @@ -// -// MarkdownTextRenderer.Visit.swift -// MarkdownView -// -// Created by LiYanan2004 on 2025/4/1. -// - -import Markdown -import Foundation -import CoreGraphics - -extension MarkdownTextRenderer: @preconcurrency MarkupVisitor { - mutating func defaultVisit(_ markup: any Markdown.Markup) -> MarkdownTextNode { - MarkdownTextNode( - kind: .unknown, - children: markup.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - environment: environment - ) - } - - mutating func visitDocument(_ document: Document) -> MarkdownTextNode { - MarkdownTextNode( - kind: .document, - children: document.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - environment: environment - ) - } - - mutating func visitText(_ text: Text) -> MarkdownTextNode { - MarkdownTextNode( - kind: .plainText, - children: [], - content: .text(text.plainText), - environment: environment - ) - } - - mutating func visitHeading(_ heading: Heading) -> MarkdownTextNode { - MarkdownTextNode( - kind: .heading, - children: heading.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - content: .heading(heading.level), - environment: environment - ) - } - - mutating func visitStrong(_ strong: Strong) -> MarkdownTextNode { - MarkdownTextNode( - kind: .boldText, - children: strong.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - environment: environment - ) - } - - mutating func visitEmphasis(_ emphasis: Emphasis) -> MarkdownTextNode { - MarkdownTextNode( - kind: .italicText, - children: emphasis.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - environment: environment - ) - } - - mutating func visitParagraph(_ paragraph: Paragraph) -> MarkdownTextNode { - MarkdownTextNode( - kind: .paragraph, - children: paragraph.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - environment: environment - ) - } - - mutating func visitStrikethrough(_ strikethrough: Strikethrough) -> MarkdownTextNode { - MarkdownTextNode( - kind: .strikethrough, - children: strikethrough.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - environment: environment - ) - } - - mutating func visitLink(_ link: Link) -> MarkdownTextNode { - if let destination = link.destination, let url = URL(string: destination) { - MarkdownTextNode( - kind: .link, - children: [], - content: .link(link.title ?? link.plainText, url), - environment: environment - ) - } else { - MarkdownTextNode( - kind: .plainText, - children: [], - content: .text(link.plainText), - environment: environment - ) - } - } - - mutating func visitThematicBreak(_ thematicBreak: ThematicBreak) -> MarkdownTextNode { - MarkdownTextNode(kind: .hardBreak, children: [], environment: environment) - } - - mutating func visitLineBreak(_ lineBreak: LineBreak) -> MarkdownTextNode { - MarkdownTextNode(kind: .hardBreak, children: [], environment: environment) - } - - mutating func visitSoftBreak(_ softBreak: SoftBreak) -> MarkdownTextNode { - MarkdownTextNode(kind: .softBreak, children: [], environment: environment) - } - - mutating func visitInlineCode(_ inlineCode: InlineCode) -> MarkdownTextNode { - MarkdownTextNode( - kind: .code, - children: [], - content: .text(inlineCode.code), - environment: environment - ) - } - - mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> MarkdownTextNode { - if let language = codeBlock.language { - MarkdownTextNode( - kind: .codeBlock, - children: [ - MarkdownTextNode( - kind: .code, - children: [], - content: .text(codeBlock.code), - environment: environment - ) - ], - content: .codeLanguage(language), - environment: environment - ) - } else { - MarkdownTextNode( - kind: .paragraph, - children: [ - MarkdownTextNode( - kind: .plainText, - children: [], - content: .text(codeBlock.code), - environment: environment - ) - ], - environment: environment - ) - } - } - - mutating func visitUnorderedList(_ unorderedList: UnorderedList) -> MarkdownTextNode { - MarkdownTextNode( - kind: .unorderedList, - children: unorderedList.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - content: nil, - depth: unorderedList.listDepth, - environment: environment - ) - } - - mutating func visitOrderedList(_ orderedList: OrderedList) -> MarkdownTextNode { - MarkdownTextNode( - kind: .orderedList, - children: orderedList.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - content: nil, - depth: orderedList.listDepth, - environment: environment - ) - } - - mutating func visitListItem(_ listItem: ListItem) -> MarkdownTextNode { - if let _ = listItem.checkbox { - MarkdownTextNode( - kind: .listItem, - children: listItem.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - content: nil, - environment: environment - ) - } else { - MarkdownTextNode( - kind: .listItem, - children: listItem.children.enumerated().map { - visit($0.element) - .with(\.index, $0.offset) - }, - content: nil, - environment: environment - ) - } - } - - mutating func visitImage(_ image: Image) -> MarkdownTextNode { - if let source = image.source, let sourceURL = URL(string: source) { - let task = Task.detached(priority: .background) { - (try await ImageLoader.load(sourceURL)) as (any Sendable) - } - return MarkdownTextNode( - kind: .placeholder, - children: [ - MarkdownTextNode( - kind: .plainText, - children: [], - content: .text(image.title ?? image.plainText), - environment: environment - ) - ], - content: .task(task), - environment: environment - ) - } else { - return MarkdownTextNode( - kind: .plainText, - children: [], - content: .text(image.plainText), - environment: environment - ) - } - } -} diff --git a/Sources/MarkdownView/WIP/MarkdownTextRenderer.swift b/Sources/MarkdownView/WIP/MarkdownTextRenderer.swift deleted file mode 100644 index d3e156de3..000000000 --- a/Sources/MarkdownView/WIP/MarkdownTextRenderer.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MarkdownTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/11. -// - -import SwiftUI - -@MainActor -@preconcurrency -struct MarkdownTextRenderer { - var environment: EnvironmentValues - - func renderMarkdownContent(_ markdownContent: MarkdownContent) -> MarkdownTextNode { - var renderer = self - return renderer.visit(markdownContent.parse()) - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/BreakTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/BreakTextRenderer.swift deleted file mode 100644 index f9fc89640..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/BreakTextRenderer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// BreakTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct BreakTextRenderer: MarkdownNode2TextRenderer { - var breakType: BreakType? - enum BreakType { - case soft - case hard - } - - func body(context: Context) -> Text { - let breakType: BreakType? = switch context.node.kind { - case .hardBreak: .hard - case .softBreak: .soft - default: self.breakType - } - - if breakType == .soft { - Text(" ") - } else if breakType == .hard { - Text("\n") - .font(.system(size: 1)) - } - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/CodeBlockTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/CodeBlockTextRenderer.swift deleted file mode 100644 index c85a44e5f..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/CodeBlockTextRenderer.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// CodeBlockTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI -#if canImport(Highlightr) -import Highlightr -#endif - -struct CodeBlockTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - if context.node.index != 0 { - BreakTextRenderer(breakType: .hard) - .body(context: context) - } - - let language = if case let .codeLanguage(language) = context.node.content! { - language - } else { - fatalError("Missing code language") - } - - if case let .text(code) = context.node.children[0].content! { - let processedCode: AttributedString = { - #if canImport(Highlightr) - let highlighter = Highlightr()! - let themeConfig = CodeHighlighterTheme( - lightModeThemeName: "xcode", - darkModeThemeName: "dark" - ) - highlighter.setTheme(to: themeConfig.themeName(for: context.environment.colorScheme)) - if highlighter.supportedLanguages().contains(language) == true, - let highlighted = highlighter.highlight(code, as: language) { - let attributedCode = NSMutableAttributedString( - attributedString: highlighted - ) - attributedCode.removeAttribute(.font, range: NSMakeRange(0, attributedCode.length)) - - return AttributedString(attributedCode) - } else { - return AttributedString(code) - } - #else - return AttributedString(code) - #endif - }() - - Text(processedCode) - .font(.callout.monospaced()) - } - } -} - -/* -@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) -struct CodeBlockTextRenderer: TextRenderer { - private func maxWidthOfTextLayout(_ layout: Text.Layout) -> CGFloat { - var maxWidth: CGFloat = 0 - for line in layout { - maxWidth = max(line.typographicBounds.width, maxWidth) - } - return maxWidth - } - - private func _drawCodeBlockBackground(rect: CGRect, padding: CGFloat, in context: inout GraphicsContext) { - context.fill( - RoundedRectangle(cornerRadius: abs(padding)) - .path(in: rect.insetBy(dx: padding, dy: padding)), - with: .color(.red.opacity(0.5)) - ) - } - - private func drawsBackgroundForCodeBlocks(layout: Text.Layout, in context: inout GraphicsContext) { - let maxWidth: CGFloat = maxWidthOfTextLayout(layout) - var codeBlockRect: CGRect? - - for line in layout { - let firstRun = line.first - guard let firstRun else { continue } - - guard firstRun.isWrappedInsideCodeBlock else { - if let codeBlockRect { - _drawCodeBlockBackground(rect: codeBlockRect, padding: -6, in: &context) - } - codeBlockRect = nil - continue - } - - if codeBlockRect == nil { - // Create a new background area - codeBlockRect = CGRect( - origin: line.typographicBounds.rect.origin, - size: CGSize( - width: maxWidth, - height: line.typographicBounds.rect.height - ) - ) - } else { - // Grows the height - codeBlockRect!.size.height = line.typographicBounds.rect.maxY - codeBlockRect!.minY - } - } - - if let codeBlockRect { - _drawCodeBlockBackground(rect: codeBlockRect, padding: -6, in: &context) - } - } - - func draw(layout: Text.Layout, in context: inout GraphicsContext) { - drawsBackgroundForCodeBlocks(layout: layout, in: &context) - - for line in layout { - context.draw(line) - } - } -} - -@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) -struct CodeBlockTextAttribute: TextAttribute {} - -// MARK: - Auxiliary - -@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) -fileprivate extension Text.Layout.Run { - var isWrappedInsideCodeBlock: Bool { - self[CodeBlockTextAttribute.self] != nil - } -} -*/ diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/FormattedTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/FormattedTextRenderer.swift deleted file mode 100644 index e5cbb545f..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/FormattedTextRenderer.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct FormattedTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - let text = context.node.children - .map { $0.render() } - .reduce(Text(""), +) - - return switch context.node.kind { - case .boldText: - text.bold() - case .italicText: - text.italic() - case .strikethrough: - text.strikethrough() - default: - fatalError() - } - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/HeadingTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/HeadingTextRenderer.swift deleted file mode 100644 index 32b2697ec..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/HeadingTextRenderer.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// HeadingTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct HeadingTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - let level = if case let .heading(level) = context.node.content { - level - } else { - -1 - } - - let font = switch level { - case 1: context.environment.markdownFontGroup.h1 - case 2: context.environment.markdownFontGroup.h2 - case 3: context.environment.markdownFontGroup.h3 - case 4: context.environment.markdownFontGroup.h4 - case 5: context.environment.markdownFontGroup.h5 - case 6: context.environment.markdownFontGroup.h6 - default: context.environment.markdownFontGroup.body - } - - let foregroundStyle = switch level { - case 1: context.environment.headingStyleGroup.h1 - case 2: context.environment.headingStyleGroup.h2 - case 3: context.environment.headingStyleGroup.h3 - case 4: context.environment.headingStyleGroup.h4 - case 5: context.environment.headingStyleGroup.h5 - case 6: context.environment.headingStyleGroup.h6 - default: AnyShapeStyle(.foreground) - } - - if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { - context.node.children - .map { $0.render() } - .reduce(Text(""), +) - .foregroundStyle(foregroundStyle) - .font(font) - } else { - context.node.children - .map { $0.render() } - .reduce(Text(""), +) - .font(font) - } - - BreakTextRenderer(breakType: .hard) - .body(context: context) - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ImageTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ImageTextRenderer.swift deleted file mode 100644 index 63247c299..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ImageTextRenderer.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ImageTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct ImageTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - if case let .image(image) = context.node.content { - image - } - } -} - -enum ImageLoader { - static func load(_ url: URL) async throws -> Image { - let (data, _) = try await URLSession.shared.data(from: url) - - #if os(macOS) - let nsImage = NSImage(data: data) - guard let nsImage else { - throw LoadError.invalidData - } - - return Image(platformImage: nsImage) - #else - let uiImage = UIImage(data: data) - guard let uiImage else { - throw LoadError.invalidData - } - - return Image(platformImage: uiImage) - #endif - } - - enum LoadError: Error { - case invalidData - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/InlineCodeTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/InlineCodeTextRenderer.swift deleted file mode 100644 index a7c198b5a..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/InlineCodeTextRenderer.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// InlineCodeTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct InlineCodeTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - if case let .text(text) = context.node.content! { - if #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) { - Text(text).monospaced() - } else { - Text(text).font(.body.monospaced()) - } - } - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/LinkTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/LinkTextRenderer.swift deleted file mode 100644 index 1e331f33f..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/LinkTextRenderer.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// LinkTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct LinkTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - if case let .link(title, url) = context.node.content! { - Text(.init("[\(title)](\(url))")) - } - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ListItemTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ListItemTextRenderer.swift deleted file mode 100644 index 3c152f9e0..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ListItemTextRenderer.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ListItemTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/13. -// - -import SwiftUI - -struct ListItemTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - context.node.children - .map { $0.render() } - .reduce(Text(""), +) - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/OrderedListTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/OrderedListTextRenderer.swift deleted file mode 100644 index dd4fb1778..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/OrderedListTextRenderer.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// OrderedListTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/13. -// - -import SwiftUI - -struct OrderedListTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - let indents = context.node.depth ?? 0 - let indentation = (0.. Text { - let marker = context.rendererConfiguration.listConfiguration - .orderedListMarker - .marker(at: index, listDepth: context.node.depth ?? 0) - if context.environment.markdownRendererConfiguration.listConfiguration.orderedListMarker.monospaced { - if #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) { - Text("\(marker) ") - .monospaced() - } else { - Text("\(marker) ") - .font(.body.monospaced()) - } - } else { - Text("\(marker) ") - } - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ParagraphTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ParagraphTextRenderer.swift deleted file mode 100644 index e96090f3e..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/ParagraphTextRenderer.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ParagraphTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import SwiftUI - -struct ParagraphTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - context.node.children - .map { $0.render() } - .reduce(Text(""), +) - BreakTextRenderer(breakType: .hard) - .body(context: context) - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/Protocol/MarkdownNode2TextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/Protocol/MarkdownNode2TextRenderer.swift deleted file mode 100644 index 0b4f89af6..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/Protocol/MarkdownNode2TextRenderer.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MarkdownNode2TextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/12. -// - -import Foundation -import SwiftUI - -@MainActor -@preconcurrency -protocol MarkdownNode2TextRenderer { - typealias Context = MarkdownNode2TextRendererContext - - @MainActor - @TextBuilder - func body(context: Context) -> Text -} - -@MainActor -@preconcurrency -struct MarkdownNode2TextRendererContext: Sendable { - var node: MarkdownTextNode - var environment: EnvironmentValues - var rendererConfiguration: MarkdownRendererConfiguration { - environment.markdownRendererConfiguration - } -} diff --git a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/UnorderedListTextRenderer.swift b/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/UnorderedListTextRenderer.swift deleted file mode 100644 index 61c73dc07..000000000 --- a/Sources/MarkdownView/WIP/Renderers/MarkdownText Components/UnorderedListTextRenderer.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// UnorderedListTextRenderer.swift -// MarkdownView -// -// Created by Yanan Li on 2025/2/13. -// - -import SwiftUI - -struct UnorderedListTextRenderer: MarkdownNode2TextRenderer { - func body(context: Context) -> Text { - let indents = context.node.depth ?? 0 - let indentation = (0.. Text { - let marker = context.rendererConfiguration.listConfiguration - .unorderedListMarker - .marker(listDepth: context.node.depth ?? 0) - if context.rendererConfiguration.listConfiguration.unorderedListMarker.monospaced { - if #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) { - Text("\(marker) ") - .monospaced() - } else { - Text("\(marker) ") - .font(.body.monospaced()) - } - } else { - Text("\(marker) ") - } - } -}