Tell the math hook the text style around each expression !12

merged merged by cmc on 2026-10-16 02:12 UTC · krz/org-swift:math-text-style into main

4 files changed, +41 −10

Layout: unified · split

README.md +5 −4
@@ -121,12 +121,13 @@ OrgView(orgSource, options: options, textTransform: { text in
121LaTeX fragments (`$…$`, `$$…$$`, `\(…\)`, `\[…\]`, `\begin{…}…\end{…}`) and 121LaTeX fragments (`$…$`, `$$…$$`, `\(…\)`, `\[…\]`, `\begin{…}…\end{…}`) and
122`\begin{…}` blocks parse to `.math` objects and `.latexBlock` elements, found by 122`\begin{…}` blocks parse to `.math` objects and `.latexBlock` elements, found by
123go-org's rules with org's border rule for `$…$`. The HTML and `AttributedString` 123go-org's rules with org's border rule for `$…$`. The HTML and `AttributedString`
124renderers show their source. `OrgView` draws them through `math`, which returns a 124renderers show their source. `OrgView` draws them through `math`, which is given the
125`Text` (an image from a typesetter, say) or nil to keep the source: 125text style of the run the expression sits in and returns a `Text` (an image from a
126typesetter, say) or nil to keep the source:
126 127
127```swift 128```swift
128OrgView(orgSource, options: options, math: { math in 129OrgView(orgSource, options: options, math: { math, style in
129 typeset(math.tex, display: math.display).map { Text($0) } 130 typeset(math.tex, display: math.display, style: style).map { Text($0) }
130}) 131})
131``` 132```
132 133
Sources/OrgSwiftUI/OrgInlineStyler.swift +4
@@ -62,6 +62,10 @@ struct OrgInlineStyler {
62 break 62 break
63 } 63 }
64 64
65 if string[range][OrgMathAttribute.self] != nil {
66 string[range][OrgMathStyleAttribute.self] = textStyle
67 }
68
65 // Always set the font, never only on modified runs: a run with no emphasis 69 // Always set the font, never only on modified runs: a run with no emphasis
66 // would otherwise inherit the surrounding view's font and a heading, table cell 70 // would otherwise inherit the surrounding view's font and a heading, table cell
67 // or footnote would render at body size. 71 // or footnote would render at body size.
Sources/OrgSwiftUI/OrgMathText.swift +12 −4
@@ -2,8 +2,15 @@ import OrgSwift
2import SwiftUI 2import SwiftUI
3 3
4/// Draws one TeX expression for ``OrgView``: a `Text` holding it, typically an image set on 4/// Draws one TeX expression for ``OrgView``: a `Text` holding it, typically an image set on
5/// the baseline, or nil to show the source as written. 5/// the baseline, or nil to show the source as written. The style is that of the text the
6public typealias OrgMathRenderer = @MainActor (OrgMath) -> Text? 6/// expression sits in, so math in a heading can be set at the heading's size.
7public typealias OrgMathRenderer = @MainActor (OrgMath, Font.TextStyle) -> Text?
8
9/// The text style a math run is set in, recorded by ``OrgInlineStyler`` for the renderer.
10enum OrgMathStyleAttribute: AttributedStringKey {
11 typealias Value = Font.TextStyle
12 static let name = "OrgSwiftUI.mathStyle"
13}
7 14
8private struct OrgMathKey: EnvironmentKey { 15private struct OrgMathKey: EnvironmentKey {
9 static let defaultValue: OrgMathRenderer? = nil 16 static let defaultValue: OrgMathRenderer? = nil
@@ -28,7 +35,8 @@ func orgText(_ string: AttributedString, math: OrgMathRenderer?) -> Text {
28 var result = Text(verbatim: "") 35 var result = Text(verbatim: "")
29 for (expression, range) in string.runs[OrgMathAttribute.self] { 36 for (expression, range) in string.runs[OrgMathAttribute.self] {
30 let piece = AttributedString(string[range]) 37 let piece = AttributedString(string[range])
31 guard let expression, let drawn = math(expression) else { 38 let style = piece.runs.first?[OrgMathStyleAttribute.self] ?? .body
39 guard let expression, let drawn = math(expression, style) else {
32 result = Text("\(result)\(Text(piece))") 40 result = Text("\(result)\(Text(piece))")
33 continue 41 continue
34 } 42 }
@@ -45,7 +53,7 @@ struct OrgMathBlockView: View {
45 @Environment(\.orgMath) private var renderer 53 @Environment(\.orgMath) private var renderer
46 54
47 var body: some View { 55 var body: some View {
48 if let drawn = renderer?(math) { 56 if let drawn = renderer?(math, .body) {
49 ViewThatFits(in: .horizontal) { 57 ViewThatFits(in: .horizontal) {
50 drawn.frame(maxWidth: .infinity) 58 drawn.frame(maxWidth: .infinity)
51 ScrollView(.horizontal, showsIndicators: false) { drawn } 59 ScrollView(.horizontal, showsIndicators: false) { drawn }
Tests/OrgSwiftUITests/OrgMathLayoutTests.swift +20 −2
@@ -48,19 +48,37 @@ struct OrgMathLayoutTests {
48 Issue.record("expected a paragraph"); return 48 Issue.record("expected a paragraph"); return
49 } 49 }
50 var drawn = 0 50 var drawn = 0
51 _ = orgText(text) { _ in 51 _ = orgText(text) { _, _ in
52 drawn += 1 52 drawn += 1
53 return Text(verbatim: "·") 53 return Text(verbatim: "·")
54 } 54 }
55 #expect(drawn == 1) 55 #expect(drawn == 1)
56 } 56 }
57 57
58 @Test func theHookIsToldTheStyleOfTheTextAroundIt() {
59 let blocks = layout("* Heading $x$\n\nbody $y$").blocks
60 var styles: [String: Font.TextStyle] = [:]
61 for block in blocks {
62 let text: AttributedString
63 switch block {
64 case .heading(let heading, _): text = heading
65 case .paragraph(let paragraph): text = paragraph
66 default: continue
67 }
68 _ = orgText(text) { math, style in
69 styles[math.tex] = style
70 return Text(verbatim: "·")
71 }
72 }
73 #expect(styles == ["x": .title, "y": .body])
74 }
75
58 @Test func orgTextDrawsEachExpression() { 76 @Test func orgTextDrawsEachExpression() {
59 guard case .paragraph(let text) = layout("$x$$x$ and $y$").blocks.first else { 77 guard case .paragraph(let text) = layout("$x$$x$ and $y$").blocks.first else {
60 Issue.record("expected a paragraph"); return 78 Issue.record("expected a paragraph"); return
61 } 79 }
62 var drawn: [String] = [] 80 var drawn: [String] = []
63 _ = orgText(text) { math in 81 _ = orgText(text) { math, _ in
64 drawn.append(math.tex) 82 drawn.append(math.tex)
65 return Text(verbatim: "·") 83 return Text(verbatim: "·")
66 } 84 }