Tell the math hook the text style around each expression !12
4 files changed, +41 −10
Layout: unified · split
README.md +5 −4
| @@ -121,12 +121,13 @@ OrgView(orgSource, options: options, textTransform: { text in | |||
| 121 | LaTeX fragments (`$…$`, `$$…$$`, `\(…\)`, `\[…\]`, `\begin{…}…\end{…}`) and | 121 | LaTeX 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 |
| 123 | go-org's rules with org's border rule for `$…$`. The HTML and `AttributedString` | 123 | go-org's rules with org's border rule for `$…$`. The HTML and `AttributedString` |
| 124 | renderers show their source. `OrgView` draws them through `math`, which returns a | 124 | renderers 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: | 125 | text style of the run the expression sits in and returns a `Text` (an image from a |
| 126 | typesetter, say) or nil to keep the source: | ||
| 126 | 127 | ||
| 127 | ```swift | 128 | ```swift |
| 128 | OrgView(orgSource, options: options, math: { math in | 129 | OrgView(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 | |||
| 2 | import SwiftUI | 2 | import 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 |
| 6 | public typealias OrgMathRenderer = @MainActor (OrgMath) -> Text? | 6 | /// expression sits in, so math in a heading can be set at the heading's size. |
| 7 | public typealias OrgMathRenderer = @MainActor (OrgMath, Font.TextStyle) -> Text? | ||
| 8 | |||
| 9 | /// The text style a math run is set in, recorded by ``OrgInlineStyler`` for the renderer. | ||
| 10 | enum OrgMathStyleAttribute: AttributedStringKey { | ||
| 11 | typealias Value = Font.TextStyle | ||
| 12 | static let name = "OrgSwiftUI.mathStyle" | ||
| 13 | } | ||
| 7 | 14 | ||
| 8 | private struct OrgMathKey: EnvironmentKey { | 15 | private 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 | } |