-
-
Notifications
You must be signed in to change notification settings - Fork 62
Enhance configuration support #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
2ebe004
b84017a
eb9e35d
5e3ae15
0702e44
87eea1b
60c38ee
c9afd41
23b469b
73ae6af
995d873
d290987
0c36ee7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,34 +3,91 @@ import Markdown | |
|
|
||
| /// A view that renders markdown content. | ||
| public struct MarkdownView: View { | ||
| @ObservedObject private var content: MarkdownContent | ||
|
|
||
| /// Owned content for the string/URL inits — created once, survives parent re‑renders. | ||
| @StateObject private var ownedContent: MarkdownContent | ||
| /// External content passed via ``init(_:)-MarkdownContent``. | ||
| @ObservedObject private var externalContent: MarkdownContent | ||
| /// Which source of truth to use. | ||
| private var usesExternalContent: Bool | ||
|
|
||
| private var content: MarkdownContent { | ||
| usesExternalContent ? externalContent : ownedContent | ||
| } | ||
|
|
||
| @Environment(\.markdownRendererConfiguration) private var configuration | ||
| @Environment(\.markdownViewRenderer) private var renderer | ||
|
|
||
| @Environment(\.headingStyleGroup) private var headingStyleGroup | ||
|
|
||
| /// Creates a view that renders given markdown string. | ||
| /// - Parameter text: The markdown source to render. | ||
| public init(_ text: String) { | ||
| self.content = MarkdownContent(text) | ||
| let content = MarkdownContent(text) | ||
| _ownedContent = StateObject(wrappedValue: content) | ||
| _externalContent = ObservedObject(wrappedValue: content) | ||
| usesExternalContent = false | ||
| } | ||
|
|
||
| /// 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(url) | ||
| let content = MarkdownContent(url) | ||
| _ownedContent = StateObject(wrappedValue: content) | ||
| _externalContent = ObservedObject(wrappedValue: content) | ||
| usesExternalContent = false | ||
| } | ||
|
|
||
| /// Creates an instance that renders from a ``MarkdownContent`` . | ||
| /// - Parameter content: The ``MarkdownContent`` to render. | ||
| public init(_ content: MarkdownContent) { | ||
| self.content = content | ||
| _ownedContent = StateObject(wrappedValue: content) | ||
| _externalContent = ObservedObject(wrappedValue: content) | ||
| usesExternalContent = true | ||
| } | ||
|
|
||
| public var body: some View { | ||
| renderer | ||
| .makeBody(content: content, configuration: configuration) | ||
| .erasedToAnyView() | ||
| .font(configuration.fonts[.body] ?? Font.body) | ||
| var config = configuration | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to modify the existing view modifier |
||
| config.headingStyleGroup = headingStyleGroup | ||
| return MarkdownRenderingView( | ||
| content: content, | ||
| configuration: config | ||
| ) | ||
| .font(configuration.fonts[.body] ?? Font.body) | ||
| } | ||
| } | ||
|
|
||
| /// A cache that stores the last rendered view alongside the inputs that | ||
| /// produced it. Because this is a reference type stored in `@State`, it | ||
| /// persists across body evaluations without triggering additional renders. | ||
| @MainActor | ||
| private final class RenderCache { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current (in v2 as well) is not efficient. Caches are rarely hit. I will implement a new approach later |
||
| var raw: MarkdownContent.Raw? | ||
| var configuration: MarkdownRendererConfiguration? | ||
| var rendered: AnyView = AnyView(EmptyView()) | ||
| } | ||
|
|
||
| /// An inner view that caches the rendered output. | ||
| /// | ||
| /// Rendering is skipped when the markdown source and configuration | ||
| /// have not changed since the last evaluation. | ||
| private struct MarkdownRenderingView: View { | ||
| let content: MarkdownContent | ||
| let configuration: MarkdownRendererConfiguration | ||
|
|
||
| @Environment(\.markdownViewRenderer) private var renderer | ||
| @State private var cache = RenderCache() | ||
|
|
||
| var body: some View { | ||
| let currentRaw = (try? content.markdown).map { MarkdownContent.Raw.plainText($0) } | ||
| if currentRaw != cache.raw || configuration != cache.configuration { | ||
| let rendered = renderer | ||
| .makeBody(content: content, configuration: configuration) | ||
| .erasedToAnyView() | ||
| cache.raw = currentRaw | ||
| cache.configuration = configuration | ||
| cache.rendered = rendered | ||
| return rendered | ||
| } | ||
| return cache.rendered | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid data corruption, we should only keep single source-of-truth. But there are two.
I know there is a flag but the reason for switching to an ObservableObject is to be able to share the content across view hierarchy so for MarkdownView, it must use
ObservedObject(the object is passed either from parent view or fromMarkdownReader