-
Notifications
You must be signed in to change notification settings - Fork 48
feat: optimize RichView styles to match Android app #80
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 1 commit
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 |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ public struct RenderStylesheet: Equatable { | |
| public var list: ListStyle | ||
| public var mention: MentionStyle | ||
| public var image: ImageStyle | ||
| public var table: TableStyle | ||
| public var horizontalRule: HorizontalRuleStyle | ||
|
|
||
| public init( | ||
| body: TextStyle = TextStyle(), | ||
|
|
@@ -26,7 +28,9 @@ public struct RenderStylesheet: Equatable { | |
| blockquote: BlockquoteStyle = BlockquoteStyle(), | ||
| list: ListStyle = ListStyle(), | ||
| mention: MentionStyle = MentionStyle(), | ||
| image: ImageStyle = ImageStyle() | ||
| image: ImageStyle = ImageStyle(), | ||
| table: TableStyle = TableStyle(), | ||
| horizontalRule: HorizontalRuleStyle = HorizontalRuleStyle() | ||
| ) { | ||
| self.body = body | ||
| self.heading = heading | ||
|
|
@@ -36,6 +40,8 @@ public struct RenderStylesheet: Equatable { | |
| self.list = list | ||
| self.mention = mention | ||
| self.image = image | ||
| self.table = table | ||
| self.horizontalRule = horizontalRule | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -257,6 +263,49 @@ public struct ImageStyle: Equatable { | |
| } | ||
| } | ||
|
|
||
| /// Table styling | ||
| public struct TableStyle: Equatable { | ||
| public var headerFontWeight: Font.Weight | ||
| public var headerBackgroundColor: Color | ||
| public var cellPadding: CGFloat | ||
| public var separatorColor: Color | ||
| public var separatorWidth: CGFloat | ||
| public var alternateRowColor: Color? | ||
|
|
||
| public init( | ||
| headerFontWeight: Font.Weight = .semibold, | ||
| headerBackgroundColor: Color = .clear, | ||
| cellPadding: CGFloat = 8, | ||
| separatorColor: Color = Color.gray.opacity(0.3), | ||
| separatorWidth: CGFloat = 0.5, | ||
| alternateRowColor: Color? = nil | ||
| ) { | ||
| self.headerFontWeight = headerFontWeight | ||
| self.headerBackgroundColor = headerBackgroundColor | ||
| self.cellPadding = cellPadding | ||
| self.separatorColor = separatorColor | ||
| self.separatorWidth = separatorWidth | ||
| self.alternateRowColor = alternateRowColor | ||
| } | ||
|
Comment on lines
267
to
295
|
||
| } | ||
|
|
||
| /// Horizontal rule styling | ||
| public struct HorizontalRuleStyle: Equatable { | ||
| public var color: Color | ||
| public var height: CGFloat | ||
| public var verticalPadding: CGFloat | ||
|
|
||
| public init( | ||
| color: Color = Color(hex: "#f4f2f2"), | ||
| height: CGFloat = 0.8, | ||
| verticalPadding: CGFloat = 8 | ||
| ) { | ||
| self.color = color | ||
| self.height = height | ||
| self.verticalPadding = verticalPadding | ||
| } | ||
| } | ||
|
Comment on lines
299
to
316
|
||
|
|
||
| // MARK: - Presets | ||
|
|
||
| extension RenderStylesheet { | ||
|
|
@@ -335,6 +384,132 @@ extension RenderStylesheet { | |
| light: Color(hex: "#d0d7de"), | ||
| dark: Color(hex: "#3d444d") | ||
| ) | ||
| ), | ||
| table: TableStyle( | ||
| separatorColor: Color.adaptive( | ||
| light: Color.gray.opacity(0.3), | ||
| dark: Color.gray.opacity(0.4) | ||
| ) | ||
| ), | ||
| horizontalRule: HorizontalRuleStyle( | ||
| color: Color.adaptive( | ||
| light: Color(hex: "#f4f2f2"), | ||
| dark: Color(hex: "#202020") | ||
| ) | ||
| ) | ||
| ) | ||
| }() | ||
|
|
||
| /// V2EX styling matching Android app | ||
| public static let v2ex: RenderStylesheet = { | ||
| RenderStylesheet( | ||
| body: TextStyle( | ||
| fontSize: 16, | ||
| fontWeight: .regular, | ||
| lineSpacing: 4, | ||
| paragraphSpacing: 8, | ||
| color: Color.adaptive( | ||
| light: Color(hex: "#555555"), | ||
| dark: Color.white.opacity(0.9) | ||
| ) | ||
| ), | ||
| heading: HeadingStyle( | ||
| h1Size: 22, | ||
| h2Size: 18, | ||
| h3Size: 16, | ||
| h4Size: 15, | ||
| h5Size: 12, | ||
| h6Size: 10, | ||
| fontWeight: .medium, | ||
| topSpacing: 15, | ||
| bottomSpacing: 15, | ||
| color: Color.adaptive( | ||
| light: Color.black, | ||
| dark: Color(hex: "#7F8080") | ||
| ) | ||
| ), | ||
| link: LinkStyle( | ||
| color: Color.adaptive( | ||
| light: Color(hex: "#778087"), | ||
| dark: Color(hex: "#58a6ff") | ||
| ), | ||
| underline: false, | ||
| fontWeight: .regular | ||
| ), | ||
| code: CodeStyle( | ||
| inlineFontSize: 13, // 80% of 16 | ||
| inlineBackgroundColor: Color.adaptive( | ||
| light: Color(hex: "#f6f8fa"), | ||
| dark: Color.clear | ||
| ), | ||
| inlineTextColor: Color.adaptive( | ||
| light: Color(hex: "#24292e"), | ||
| dark: Color(hex: "#7F8082") | ||
| ), | ||
| blockFontSize: 13, | ||
| blockBackgroundColor: Color.adaptive( | ||
| light: Color(hex: "#f6f8fa"), | ||
| dark: Color(hex: "#111214") | ||
| ), | ||
| blockTextColor: Color.adaptive( | ||
| light: Color(hex: "#24292e"), | ||
| dark: Color(hex: "#7F8082") | ||
| ), | ||
| highlightTheme: .tomorrowNight | ||
| ), | ||
| blockquote: BlockquoteStyle( | ||
| borderColor: Color(hex: "#7e7e7e").opacity(0.5), | ||
| borderWidth: 3, | ||
| backgroundColor: Color.adaptive( | ||
| light: Color(hex: "#fafafa").opacity(0.5), | ||
| dark: Color(hex: "#08090b") | ||
| ), | ||
| padding: EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5), | ||
| fontSize: 15 | ||
| ), | ||
| list: ListStyle( | ||
| indentWidth: 16, | ||
| itemSpacing: 4, | ||
| bulletColor: Color.adaptive( | ||
| light: Color(hex: "#555555"), | ||
| dark: Color.white.opacity(0.9) | ||
| ), | ||
| numberColor: Color.adaptive( | ||
| light: Color(hex: "#555555"), | ||
| dark: Color.white.opacity(0.9) | ||
| ) | ||
| ), | ||
| mention: MentionStyle( | ||
| textColor: Color.adaptive( | ||
| light: Color(hex: "#778087"), | ||
| dark: Color(hex: "#58a6ff") | ||
| ), | ||
| backgroundColor: Color.adaptive( | ||
| light: Color(hex: "#778087").opacity(0.1), | ||
| dark: Color(hex: "#58a6ff").opacity(0.15) | ||
| ), | ||
| fontWeight: .medium | ||
| ), | ||
| image: ImageStyle( | ||
| maxHeight: 400, | ||
| cornerRadius: 8, | ||
| borderColor: .clear, | ||
| borderWidth: 0 | ||
| ), | ||
| table: TableStyle( | ||
| headerFontWeight: .medium, | ||
| separatorColor: Color.adaptive( | ||
| light: Color(hex: "#f4f2f2"), | ||
| dark: Color(hex: "#202020") | ||
| ), | ||
| separatorWidth: 0.5 | ||
| ), | ||
| horizontalRule: HorizontalRuleStyle( | ||
| color: Color.adaptive( | ||
| light: Color(hex: "#f4f2f2"), | ||
| dark: Color(hex: "#202020") | ||
| ), | ||
| height: 0.8 | ||
| ) | ||
| ) | ||
| }() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,7 +79,7 @@ public class MarkdownRenderer { | |
| attributedString.append(renderListItem(content, ordered: true, number: number)) | ||
| } else if line.starts(with: "---") { | ||
| // Horizontal rule | ||
| attributedString.append(AttributedString("—————————————\n")) | ||
| attributedString.append(renderHorizontalRule()) | ||
|
||
| } else if line.starts(with: "|") && line.hasSuffix("|") { | ||
| // Markdown table | ||
| let (tableBlock, linesConsumed) = extractTableBlock(lines, startIndex: index) | ||
|
|
@@ -169,6 +169,17 @@ public class MarkdownRenderer { | |
| return attributed | ||
| } | ||
|
|
||
| // MARK: - Horizontal Rule Rendering | ||
|
|
||
| private func renderHorizontalRule() -> AttributedString { | ||
| var attributed = AttributedString("\n") | ||
| var line = AttributedString(String(repeating: "─", count: 40)) | ||
| line.foregroundColor = stylesheet.horizontalRule.color.uiColor | ||
| attributed.append(line) | ||
| attributed.append(AttributedString("\n\n")) | ||
| return attributed | ||
| } | ||
|
|
||
| // MARK: - List Rendering | ||
|
|
||
| private func renderListItem(_ text: String, ordered: Bool, number: Int) -> AttributedString { | ||
|
|
@@ -342,6 +353,66 @@ public class MarkdownRenderer { | |
| continue | ||
| } | ||
|
|
||
| // Check for underline (<u>text</u>) | ||
| if let underlineMatch = currentText.firstMatch(of: /<u>(.+?)<\/u>/) { | ||
| // Add text before underline | ||
| let beforeRange = currentText.startIndex..<underlineMatch.range.lowerBound | ||
| if !beforeRange.isEmpty { | ||
| result.append(renderPlainText(String(currentText[beforeRange]))) | ||
| } | ||
|
|
||
| // Add underlined text | ||
| var underlineText = AttributedString(String(underlineMatch.1)) | ||
| underlineText.font = .system(size: stylesheet.body.fontSize) | ||
| underlineText.foregroundColor = stylesheet.body.color.uiColor | ||
| underlineText.underlineStyle = .single | ||
| result.append(underlineText) | ||
|
|
||
| // Continue with remaining text | ||
| currentText = String(currentText[underlineMatch.range.upperBound...]) | ||
| continue | ||
| } | ||
|
|
||
| // Check for superscript (<sup>text</sup>) | ||
| if let supMatch = currentText.firstMatch(of: /<sup>(.+?)<\/sup>/) { | ||
| // Add text before superscript | ||
| let beforeRange = currentText.startIndex..<supMatch.range.lowerBound | ||
| if !beforeRange.isEmpty { | ||
| result.append(renderPlainText(String(currentText[beforeRange]))) | ||
| } | ||
|
|
||
| // Add superscript text (smaller font, baseline offset) | ||
| var supText = AttributedString(String(supMatch.1)) | ||
| supText.font = .system(size: stylesheet.body.fontSize * 0.7) | ||
| supText.foregroundColor = stylesheet.body.color.uiColor | ||
| supText.baselineOffset = stylesheet.body.fontSize * 0.3 | ||
| result.append(supText) | ||
|
|
||
| // Continue with remaining text | ||
| currentText = String(currentText[supMatch.range.upperBound...]) | ||
| continue | ||
| } | ||
|
|
||
| // Check for subscript (<sub>text</sub>) | ||
| if let subMatch = currentText.firstMatch(of: /<sub>(.+?)<\/sub>/) { | ||
| // Add text before subscript | ||
| let beforeRange = currentText.startIndex..<subMatch.range.lowerBound | ||
| if !beforeRange.isEmpty { | ||
| result.append(renderPlainText(String(currentText[beforeRange]))) | ||
| } | ||
|
|
||
| // Add subscript text (smaller font, negative baseline offset) | ||
| var subText = AttributedString(String(subMatch.1)) | ||
| subText.font = .system(size: stylesheet.body.fontSize * 0.7) | ||
| subText.foregroundColor = stylesheet.body.color.uiColor | ||
| subText.baselineOffset = -stylesheet.body.fontSize * 0.2 | ||
| result.append(subText) | ||
|
|
||
| // Continue with remaining text | ||
| currentText = String(currentText[subMatch.range.upperBound...]) | ||
| continue | ||
| } | ||
|
Comment on lines
+356
to
+414
|
||
|
|
||
| // No more special elements, add remaining text | ||
| result.append(renderPlainText(currentText)) | ||
| break | ||
|
|
@@ -432,15 +503,15 @@ public class MarkdownRenderer { | |
|
|
||
| // Apply header style for first row | ||
| if rowIndex == 0 { | ||
| cellText.font = .system(size: stylesheet.body.fontSize, weight: .semibold) | ||
| cellText.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.table.headerFontWeight) | ||
| } | ||
|
|
||
| result.append(cellText) | ||
|
|
||
| // Add separator between cells | ||
| if cellIndex < row.count - 1 { | ||
| var separator = AttributedString(" │ ") | ||
| separator.foregroundColor = Color.gray.opacity(0.5) | ||
| separator.foregroundColor = stylesheet.table.separatorColor.uiColor | ||
| result.append(separator) | ||
| } | ||
| } | ||
|
|
@@ -450,7 +521,7 @@ public class MarkdownRenderer { | |
| // Add separator line after header | ||
| if rowIndex == 0 && rows.count > 1 { | ||
| var separatorLine = AttributedString(String(repeating: "─", count: 40) + "\n") | ||
| separatorLine.foregroundColor = Color.gray.opacity(0.3) | ||
| separatorLine.foregroundColor = stylesheet.table.separatorColor.uiColor | ||
| result.append(separatorLine) | ||
| } | ||
| } | ||
|
|
||
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.
Typo: "coresponding" should be spelled "corresponding".