Skip to content

Commit 38a7aa3

Browse files
graycreateclaude
andcommitted
feat: optimize RichView styles to match Android app
Add V2EX preset stylesheet with styles matching the Android app: - Smaller heading sizes (h1: 22, h2: 18, h3: 16, h4: 15, h5: 12, h6: 10) - Medium font weight for headings (matching Android's 500) - Body text color #555555 (gray) for light mode - Link color #778087 (grayish) matching V2EX web style - Blockquote: 3px left border with subtle background - Code: 80% font size (13px) relative to body Add new styling components: - TableStyle: header font weight, separator color, cell padding - HorizontalRuleStyle: color, height, vertical padding Add rendering support for HTML tags: - <u> underline with underlineStyle attribute - <sup> superscript with smaller font and positive baseline offset - <sub> subscript with smaller font and negative baseline offset Update MarkdownRenderer to use stylesheet styles for: - Horizontal rules - Table headers and separators 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 75f5d90 commit 38a7aa3

File tree

3 files changed

+253
-6
lines changed

3 files changed

+253
-6
lines changed

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,5 @@ fastlane sync_certificates
159159
- Website submodule: Located at `website/` (separate repository)
160160
- Create PR should always use English
161161
- **CHANGELOG.md is required** for all releases - the build will fail if the current version is missing from the changelog
162-
- Always install to Gray'iPhone if it connected, otherwise install to simulator
162+
- Always install to Gray'iPhone if it connected, otherwise install to simulator
163+
- the coresponding android project located in ../v2er-android

V2er/Sources/RichView/Models/RenderStylesheet.swift

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public struct RenderStylesheet: Equatable {
1717
public var list: ListStyle
1818
public var mention: MentionStyle
1919
public var image: ImageStyle
20+
public var table: TableStyle
21+
public var horizontalRule: HorizontalRuleStyle
2022

2123
public init(
2224
body: TextStyle = TextStyle(),
@@ -26,7 +28,9 @@ public struct RenderStylesheet: Equatable {
2628
blockquote: BlockquoteStyle = BlockquoteStyle(),
2729
list: ListStyle = ListStyle(),
2830
mention: MentionStyle = MentionStyle(),
29-
image: ImageStyle = ImageStyle()
31+
image: ImageStyle = ImageStyle(),
32+
table: TableStyle = TableStyle(),
33+
horizontalRule: HorizontalRuleStyle = HorizontalRuleStyle()
3034
) {
3135
self.body = body
3236
self.heading = heading
@@ -36,6 +40,8 @@ public struct RenderStylesheet: Equatable {
3640
self.list = list
3741
self.mention = mention
3842
self.image = image
43+
self.table = table
44+
self.horizontalRule = horizontalRule
3945
}
4046
}
4147

@@ -257,6 +263,49 @@ public struct ImageStyle: Equatable {
257263
}
258264
}
259265

266+
/// Table styling
267+
public struct TableStyle: Equatable {
268+
public var headerFontWeight: Font.Weight
269+
public var headerBackgroundColor: Color
270+
public var cellPadding: CGFloat
271+
public var separatorColor: Color
272+
public var separatorWidth: CGFloat
273+
public var alternateRowColor: Color?
274+
275+
public init(
276+
headerFontWeight: Font.Weight = .semibold,
277+
headerBackgroundColor: Color = .clear,
278+
cellPadding: CGFloat = 8,
279+
separatorColor: Color = Color.gray.opacity(0.3),
280+
separatorWidth: CGFloat = 0.5,
281+
alternateRowColor: Color? = nil
282+
) {
283+
self.headerFontWeight = headerFontWeight
284+
self.headerBackgroundColor = headerBackgroundColor
285+
self.cellPadding = cellPadding
286+
self.separatorColor = separatorColor
287+
self.separatorWidth = separatorWidth
288+
self.alternateRowColor = alternateRowColor
289+
}
290+
}
291+
292+
/// Horizontal rule styling
293+
public struct HorizontalRuleStyle: Equatable {
294+
public var color: Color
295+
public var height: CGFloat
296+
public var verticalPadding: CGFloat
297+
298+
public init(
299+
color: Color = Color(hex: "#f4f2f2"),
300+
height: CGFloat = 0.8,
301+
verticalPadding: CGFloat = 8
302+
) {
303+
self.color = color
304+
self.height = height
305+
self.verticalPadding = verticalPadding
306+
}
307+
}
308+
260309
// MARK: - Presets
261310

262311
extension RenderStylesheet {
@@ -335,6 +384,132 @@ extension RenderStylesheet {
335384
light: Color(hex: "#d0d7de"),
336385
dark: Color(hex: "#3d444d")
337386
)
387+
),
388+
table: TableStyle(
389+
separatorColor: Color.adaptive(
390+
light: Color.gray.opacity(0.3),
391+
dark: Color.gray.opacity(0.4)
392+
)
393+
),
394+
horizontalRule: HorizontalRuleStyle(
395+
color: Color.adaptive(
396+
light: Color(hex: "#f4f2f2"),
397+
dark: Color(hex: "#202020")
398+
)
399+
)
400+
)
401+
}()
402+
403+
/// V2EX styling matching Android app
404+
public static let v2ex: RenderStylesheet = {
405+
RenderStylesheet(
406+
body: TextStyle(
407+
fontSize: 16,
408+
fontWeight: .regular,
409+
lineSpacing: 4,
410+
paragraphSpacing: 8,
411+
color: Color.adaptive(
412+
light: Color(hex: "#555555"),
413+
dark: Color.white.opacity(0.9)
414+
)
415+
),
416+
heading: HeadingStyle(
417+
h1Size: 22,
418+
h2Size: 18,
419+
h3Size: 16,
420+
h4Size: 15,
421+
h5Size: 12,
422+
h6Size: 10,
423+
fontWeight: .medium,
424+
topSpacing: 15,
425+
bottomSpacing: 15,
426+
color: Color.adaptive(
427+
light: Color.black,
428+
dark: Color(hex: "#7F8080")
429+
)
430+
),
431+
link: LinkStyle(
432+
color: Color.adaptive(
433+
light: Color(hex: "#778087"),
434+
dark: Color(hex: "#58a6ff")
435+
),
436+
underline: false,
437+
fontWeight: .regular
438+
),
439+
code: CodeStyle(
440+
inlineFontSize: 13, // 80% of 16
441+
inlineBackgroundColor: Color.adaptive(
442+
light: Color(hex: "#f6f8fa"),
443+
dark: Color.clear
444+
),
445+
inlineTextColor: Color.adaptive(
446+
light: Color(hex: "#24292e"),
447+
dark: Color(hex: "#7F8082")
448+
),
449+
blockFontSize: 13,
450+
blockBackgroundColor: Color.adaptive(
451+
light: Color(hex: "#f6f8fa"),
452+
dark: Color(hex: "#111214")
453+
),
454+
blockTextColor: Color.adaptive(
455+
light: Color(hex: "#24292e"),
456+
dark: Color(hex: "#7F8082")
457+
),
458+
highlightTheme: .tomorrowNight
459+
),
460+
blockquote: BlockquoteStyle(
461+
borderColor: Color(hex: "#7e7e7e").opacity(0.5),
462+
borderWidth: 3,
463+
backgroundColor: Color.adaptive(
464+
light: Color(hex: "#fafafa").opacity(0.5),
465+
dark: Color(hex: "#08090b")
466+
),
467+
padding: EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5),
468+
fontSize: 15
469+
),
470+
list: ListStyle(
471+
indentWidth: 16,
472+
itemSpacing: 4,
473+
bulletColor: Color.adaptive(
474+
light: Color(hex: "#555555"),
475+
dark: Color.white.opacity(0.9)
476+
),
477+
numberColor: Color.adaptive(
478+
light: Color(hex: "#555555"),
479+
dark: Color.white.opacity(0.9)
480+
)
481+
),
482+
mention: MentionStyle(
483+
textColor: Color.adaptive(
484+
light: Color(hex: "#778087"),
485+
dark: Color(hex: "#58a6ff")
486+
),
487+
backgroundColor: Color.adaptive(
488+
light: Color(hex: "#778087").opacity(0.1),
489+
dark: Color(hex: "#58a6ff").opacity(0.15)
490+
),
491+
fontWeight: .medium
492+
),
493+
image: ImageStyle(
494+
maxHeight: 400,
495+
cornerRadius: 8,
496+
borderColor: .clear,
497+
borderWidth: 0
498+
),
499+
table: TableStyle(
500+
headerFontWeight: .medium,
501+
separatorColor: Color.adaptive(
502+
light: Color(hex: "#f4f2f2"),
503+
dark: Color(hex: "#202020")
504+
),
505+
separatorWidth: 0.5
506+
),
507+
horizontalRule: HorizontalRuleStyle(
508+
color: Color.adaptive(
509+
light: Color(hex: "#f4f2f2"),
510+
dark: Color(hex: "#202020")
511+
),
512+
height: 0.8
338513
)
339514
)
340515
}()

V2er/Sources/RichView/Renderers/MarkdownRenderer.swift

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public class MarkdownRenderer {
7979
attributedString.append(renderListItem(content, ordered: true, number: number))
8080
} else if line.starts(with: "---") {
8181
// Horizontal rule
82-
attributedString.append(AttributedString("—————————————\n"))
82+
attributedString.append(renderHorizontalRule())
8383
} else if line.starts(with: "|") && line.hasSuffix("|") {
8484
// Markdown table
8585
let (tableBlock, linesConsumed) = extractTableBlock(lines, startIndex: index)
@@ -169,6 +169,17 @@ public class MarkdownRenderer {
169169
return attributed
170170
}
171171

172+
// MARK: - Horizontal Rule Rendering
173+
174+
private func renderHorizontalRule() -> AttributedString {
175+
var attributed = AttributedString("\n")
176+
var line = AttributedString(String(repeating: "", count: 40))
177+
line.foregroundColor = stylesheet.horizontalRule.color.uiColor
178+
attributed.append(line)
179+
attributed.append(AttributedString("\n\n"))
180+
return attributed
181+
}
182+
172183
// MARK: - List Rendering
173184

174185
private func renderListItem(_ text: String, ordered: Bool, number: Int) -> AttributedString {
@@ -342,6 +353,66 @@ public class MarkdownRenderer {
342353
continue
343354
}
344355

356+
// Check for underline (<u>text</u>)
357+
if let underlineMatch = currentText.firstMatch(of: /<u>(.+?)<\/u>/) {
358+
// Add text before underline
359+
let beforeRange = currentText.startIndex..<underlineMatch.range.lowerBound
360+
if !beforeRange.isEmpty {
361+
result.append(renderPlainText(String(currentText[beforeRange])))
362+
}
363+
364+
// Add underlined text
365+
var underlineText = AttributedString(String(underlineMatch.1))
366+
underlineText.font = .system(size: stylesheet.body.fontSize)
367+
underlineText.foregroundColor = stylesheet.body.color.uiColor
368+
underlineText.underlineStyle = .single
369+
result.append(underlineText)
370+
371+
// Continue with remaining text
372+
currentText = String(currentText[underlineMatch.range.upperBound...])
373+
continue
374+
}
375+
376+
// Check for superscript (<sup>text</sup>)
377+
if let supMatch = currentText.firstMatch(of: /<sup>(.+?)<\/sup>/) {
378+
// Add text before superscript
379+
let beforeRange = currentText.startIndex..<supMatch.range.lowerBound
380+
if !beforeRange.isEmpty {
381+
result.append(renderPlainText(String(currentText[beforeRange])))
382+
}
383+
384+
// Add superscript text (smaller font, baseline offset)
385+
var supText = AttributedString(String(supMatch.1))
386+
supText.font = .system(size: stylesheet.body.fontSize * 0.7)
387+
supText.foregroundColor = stylesheet.body.color.uiColor
388+
supText.baselineOffset = stylesheet.body.fontSize * 0.3
389+
result.append(supText)
390+
391+
// Continue with remaining text
392+
currentText = String(currentText[supMatch.range.upperBound...])
393+
continue
394+
}
395+
396+
// Check for subscript (<sub>text</sub>)
397+
if let subMatch = currentText.firstMatch(of: /<sub>(.+?)<\/sub>/) {
398+
// Add text before subscript
399+
let beforeRange = currentText.startIndex..<subMatch.range.lowerBound
400+
if !beforeRange.isEmpty {
401+
result.append(renderPlainText(String(currentText[beforeRange])))
402+
}
403+
404+
// Add subscript text (smaller font, negative baseline offset)
405+
var subText = AttributedString(String(subMatch.1))
406+
subText.font = .system(size: stylesheet.body.fontSize * 0.7)
407+
subText.foregroundColor = stylesheet.body.color.uiColor
408+
subText.baselineOffset = -stylesheet.body.fontSize * 0.2
409+
result.append(subText)
410+
411+
// Continue with remaining text
412+
currentText = String(currentText[subMatch.range.upperBound...])
413+
continue
414+
}
415+
345416
// No more special elements, add remaining text
346417
result.append(renderPlainText(currentText))
347418
break
@@ -432,15 +503,15 @@ public class MarkdownRenderer {
432503

433504
// Apply header style for first row
434505
if rowIndex == 0 {
435-
cellText.font = .system(size: stylesheet.body.fontSize, weight: .semibold)
506+
cellText.font = .system(size: stylesheet.body.fontSize, weight: stylesheet.table.headerFontWeight)
436507
}
437508

438509
result.append(cellText)
439510

440511
// Add separator between cells
441512
if cellIndex < row.count - 1 {
442513
var separator = AttributedString("")
443-
separator.foregroundColor = Color.gray.opacity(0.5)
514+
separator.foregroundColor = stylesheet.table.separatorColor.uiColor
444515
result.append(separator)
445516
}
446517
}
@@ -450,7 +521,7 @@ public class MarkdownRenderer {
450521
// Add separator line after header
451522
if rowIndex == 0 && rows.count > 1 {
452523
var separatorLine = AttributedString(String(repeating: "", count: 40) + "\n")
453-
separatorLine.foregroundColor = Color.gray.opacity(0.3)
524+
separatorLine.foregroundColor = stylesheet.table.separatorColor.uiColor
454525
result.append(separatorLine)
455526
}
456527
}

0 commit comments

Comments
 (0)