Commit e2c5e4c93a
Verified · cmc
Layout: unified · split
gitbay.xcodeproj/project.pbxproj +1 −1
| @@ -603,7 +603,7 @@ | |||
| 603 | repositoryURL = "ssh://git@gitbay.org/krz/org-swift.git"; | 603 | repositoryURL = "ssh://git@gitbay.org/krz/org-swift.git"; |
| 604 | requirement = { | 604 | requirement = { |
| 605 | kind = upToNextMinorVersion; | 605 | kind = upToNextMinorVersion; |
| 606 | minimumVersion = 0.7.1; | 606 | minimumVersion = 0.8.0; |
| 607 | }; | 607 | }; |
| 608 | }; | 608 | }; |
| 609 | 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */ = { | 609 | 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */ = { |
gitbay.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +2 −2
| @@ -15,8 +15,8 @@ | |||
| 15 | "kind" : "remoteSourceControl", | 15 | "kind" : "remoteSourceControl", |
| 16 | "location" : "ssh://git@gitbay.org/krz/org-swift.git", | 16 | "location" : "ssh://git@gitbay.org/krz/org-swift.git", |
| 17 | "state" : { | 17 | "state" : { |
| 18 | "revision" : "fa46c3f1577091385a95ac5f5680409f09701004", | 18 | "revision" : "49b729cc966ddb8270e0184b0e4dd02a4e1956e8", |
| 19 | "version" : "0.7.1" | 19 | "version" : "0.8.0" |
| 20 | } | 20 | } |
| 21 | }, | 21 | }, |
| 22 | { | 22 | { |
gitbay/Views/Repos/FormattedBodyView.swift +5 −1
| @@ -13,17 +13,21 @@ struct FormattedBodyView: View { | |||
| 13 | var repo: String? | 13 | var repo: String? |
| 14 | 14 | ||
| 15 | @Environment(\.colorScheme) private var colorScheme | 15 | @Environment(\.colorScheme) private var colorScheme |
| 16 | @Environment(\.dynamicTypeSize) private var dynamicTypeSize | ||
| 17 | @State private var allowance = MathAllowance() | ||
| 16 | 18 | ||
| 17 | var isOrg: Bool { format == "org" } | 19 | var isOrg: Bool { format == "org" } |
| 18 | 20 | ||
| 19 | var body: some View { | 21 | var body: some View { |
| 20 | if isOrg { | 22 | if isOrg { |
| 23 | let _ = allowance.reset(for: text) | ||
| 21 | // Qualified: `OrgView` here is org-mode, not the organization view. | 24 | // Qualified: `OrgView` here is org-mode, not the organization view. |
| 22 | OrgSwiftUI.OrgView( | 25 | OrgSwiftUI.OrgView( |
| 23 | text, | 26 | text, |
| 24 | styler: OrgSyntaxStyler(colorScheme: colorScheme), | 27 | styler: OrgSyntaxStyler(colorScheme: colorScheme), |
| 25 | keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone), | 28 | keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone), |
| 26 | textTransform: repo.map { repo in { linkReferences(in: &$0, repo: repo) } } | 29 | textTransform: repo.map { repo in { linkReferences(in: &$0, repo: repo) } }, |
| 30 | math: MathRenderer.org(pointSize: UIFont.pointSize(.body, dynamicTypeSize), allowance: allowance) | ||
| 27 | ) | 31 | ) |
| 28 | } else { | 32 | } else { |
| 29 | MarkdownView(markdown: text, referenceRepo: repo) | 33 | MarkdownView(markdown: text, referenceRepo: repo) |
gitbay/Views/Repos/ReadmeView.swift +5 −1
| @@ -14,12 +14,15 @@ struct ReadmeView: View { | |||
| 14 | var repoPath: String = "" | 14 | var repoPath: String = "" |
| 15 | 15 | ||
| 16 | @Environment(\.colorScheme) private var colorScheme | 16 | @Environment(\.colorScheme) private var colorScheme |
| 17 | @Environment(\.dynamicTypeSize) private var dynamicTypeSize | ||
| 18 | @State private var allowance = MathAllowance() | ||
| 17 | 19 | ||
| 18 | /// Format follows the file name, never a guess at the content. | 20 | /// Format follows the file name, never a guess at the content. |
| 19 | var isOrg: Bool { name.lowercased().hasSuffix(".org") } | 21 | var isOrg: Bool { name.lowercased().hasSuffix(".org") } |
| 20 | 22 | ||
| 21 | var body: some View { | 23 | var body: some View { |
| 22 | if isOrg { | 24 | if isOrg { |
| 25 | let _ = allowance.reset(for: content) | ||
| 23 | // Qualified: `OrgView` here is org-mode, not the organization view. | 26 | // Qualified: `OrgView` here is org-mode, not the organization view. |
| 24 | OrgSwiftUI.OrgView( | 27 | OrgSwiftUI.OrgView( |
| 25 | Self.preferringPNGBadges(content, host: host), | 28 | Self.preferringPNGBadges(content, host: host), |
| @@ -27,7 +30,8 @@ struct ReadmeView: View { | |||
| 27 | styler: OrgSyntaxStyler(colorScheme: colorScheme), | 30 | styler: OrgSyntaxStyler(colorScheme: colorScheme), |
| 28 | // org's TODO/DONE are the same open/done distinction the forge already | 31 | // org's TODO/DONE are the same open/done distinction the forge already |
| 29 | // colors, so they reuse those tokens rather than a third treatment. | 32 | // colors, so they reuse those tokens rather than a third treatment. |
| 30 | keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone) | 33 | keywords: OrgKeywordStyle(todo: .gbOK, done: .gbDone), |
| 34 | math: MathRenderer.org(pointSize: UIFont.pointSize(.body, dynamicTypeSize), allowance: allowance) | ||
| 31 | ) | 35 | ) |
| 32 | } else { | 36 | } else { |
| 33 | MarkdownView(markdown: content, linkOptions: renderOptions) | 37 | MarkdownView(markdown: content, linkOptions: renderOptions) |
gitbay/Views/Shared/MathRendering.swift +44 −4
| @@ -1,3 +1,5 @@ | |||
| 1 | import OrgSwift | ||
| 2 | import OrgSwiftUI | ||
| 1 | import SwiftMath | 3 | import SwiftMath |
| 2 | import SwiftUI | 4 | import SwiftUI |
| 3 | import UIKit | 5 | import UIKit |
| @@ -159,13 +161,51 @@ enum MathRenderer { | |||
| 159 | /// The expression as text: the image, set on the baseline and tinted | 161 | /// The expression as text: the image, set on the baseline and tinted |
| 160 | /// like the text around it, or the source where it is refused. | 162 | /// like the text around it, or the source where it is refused. |
| 161 | static func text(_ math: MathExpression, pointSize: CGFloat) -> Text { | 163 | static func text(_ math: MathExpression, pointSize: CGFloat) -> Text { |
| 162 | guard let rendered = render(math.tex, display: math.display, pointSize: pointSize) else { | 164 | drawn(math.tex, display: math.display, pointSize: pointSize) ?? Text(verbatim: math.source) |
| 163 | return Text(verbatim: math.source) | 165 | } |
| 164 | } | 166 | |
| 165 | return Text(Image(rendered.image, scale: rendered.scale, label: Text(verbatim: math.tex)) | 167 | static func drawn(_ tex: String, display: Bool, pointSize: CGFloat) -> Text? { |
| 168 | guard let rendered = render(tex, display: display, pointSize: pointSize) else { return nil } | ||
| 169 | return Text(Image(rendered.image, scale: rendered.scale, label: Text(verbatim: tex)) | ||
| 166 | .renderingMode(.template)) | 170 | .renderingMode(.template)) |
| 167 | .baselineOffset(-rendered.descent) | 171 | .baselineOffset(-rendered.descent) |
| 168 | } | 172 | } |
| 173 | |||
| 174 | /// `OrgView`'s math hook: each expression the allowance admits, drawn at `pointSize`. | ||
| 175 | static func org(pointSize: CGFloat, allowance: MathAllowance) -> OrgMathRenderer { | ||
| 176 | { math in | ||
| 177 | allowance.admits(math) ? drawn(math.tex, display: math.display, pointSize: pointSize) : nil | ||
| 178 | } | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 182 | /// The web's per-document budget for org, which is drawn view by view: the first | ||
| 183 | /// expressions asked for are admitted, and keep their place on later passes. | ||
| 184 | final class MathAllowance { | ||
| 185 | private let start: MathBudget | ||
| 186 | private var budget: MathBudget | ||
| 187 | private var admitted: Set<OrgMath> = [] | ||
| 188 | private var text: String? | ||
| 189 | |||
| 190 | init(budget: MathBudget = .document) { | ||
| 191 | start = budget | ||
| 192 | self.budget = budget | ||
| 193 | } | ||
| 194 | |||
| 195 | /// Starts over when the document changes. | ||
| 196 | func reset(for text: String) { | ||
| 197 | guard text != self.text else { return } | ||
| 198 | self.text = text | ||
| 199 | budget = start | ||
| 200 | admitted = [] | ||
| 201 | } | ||
| 202 | |||
| 203 | func admits(_ math: OrgMath) -> Bool { | ||
| 204 | if admitted.contains(math) { return true } | ||
| 205 | guard budget.take(math.tex) else { return false } | ||
| 206 | admitted.insert(math) | ||
| 207 | return true | ||
| 208 | } | ||
| 169 | } | 209 | } |
| 170 | 210 | ||
| 171 | /// A display block: centred, scrolling sideways when wider than the screen, | 211 | /// A display block: centred, scrolling sideways when wider than the screen, |
gitbayTests/MathTests.swift +34
| @@ -1,6 +1,7 @@ | |||
| 1 | import Foundation | 1 | import Foundation |
| 2 | import Testing | 2 | import Testing |
| 3 | @testable import gitbay | 3 | @testable import gitbay |
| 4 | import OrgSwift | ||
| 4 | 5 | ||
| 5 | /// The web's delimiter rules (internal/httpd/math.go): pandoc's `$…$` so | 6 | /// The web's delimiter rules (internal/httpd/math.go): pandoc's `$…$` so |
| 6 | /// prices stay prose, and `$$…$$` inline or as a block. | 7 | /// prices stay prose, and `$$…$$` inline or as a block. |
| @@ -187,3 +188,36 @@ struct MathRenderingTests { | |||
| 187 | } | 188 | } |
| 188 | } | 189 | } |
| 189 | } | 190 | } |
| 191 | |||
| 192 | @MainActor | ||
| 193 | struct OrgMathHookTests { | ||
| 194 | |||
| 195 | private func org(_ tex: String, display: Bool = false) -> OrgMath { | ||
| 196 | OrgMath(tex: tex, display: display, source: "$" + tex + "$") | ||
| 197 | } | ||
| 198 | |||
| 199 | @Test func drawsWhatRendersAndKeepsTheSourceOtherwise() { | ||
| 200 | let draw = MathRenderer.org(pointSize: 17, allowance: MathAllowance()) | ||
| 201 | #expect(draw(org("x^2")) != nil) | ||
| 202 | #expect(draw(org(#"\notacommand"#)) == nil) | ||
| 203 | } | ||
| 204 | |||
| 205 | @Test func theAllowanceIsTheWebsDocumentBudget() { | ||
| 206 | let allowance = MathAllowance(budget: MathBudget(expressions: 2, bytes: 100)) | ||
| 207 | #expect(allowance.admits(org("a"))) | ||
| 208 | #expect(allowance.admits(org("b"))) | ||
| 209 | #expect(allowance.admits(org("a")), "an expression drawn once is drawn again on the next pass") | ||
| 210 | #expect(!allowance.admits(org("c"))) | ||
| 211 | } | ||
| 212 | |||
| 213 | @Test func theAllowanceStartsOverForNewText() { | ||
| 214 | let allowance = MathAllowance(budget: MathBudget(expressions: 1, bytes: 100)) | ||
| 215 | allowance.reset(for: "one") | ||
| 216 | #expect(allowance.admits(org("a"))) | ||
| 217 | #expect(!allowance.admits(org("b"))) | ||
| 218 | allowance.reset(for: "one") | ||
| 219 | #expect(!allowance.admits(org("b")), "the same text keeps its allowance") | ||
| 220 | allowance.reset(for: "two") | ||
| 221 | #expect(allowance.admits(org("b"))) | ||
| 222 | } | ||
| 223 | } | ||