Bug gixes, features added, GUI updates and more
This commit is contained in:
@@ -16,7 +16,7 @@ struct MarkdownContentView: View {
|
||||
|
||||
var body: some View {
|
||||
let segments = parseSegments(content)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
ForEach(segments.indices, id: \.self) { index in
|
||||
switch segments[index] {
|
||||
case .text(let text):
|
||||
@@ -25,6 +25,8 @@ struct MarkdownContentView: View {
|
||||
}
|
||||
case .codeBlock(let language, let code):
|
||||
CodeBlockView(language: language, code: code, fontSize: fontSize)
|
||||
case .table(let tableText):
|
||||
TableView(content: tableText, fontSize: fontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,16 +34,18 @@ struct MarkdownContentView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func markdownText(_ text: String) -> some View {
|
||||
if let attrString = try? AttributedString(markdown: text, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
|
||||
if let attrString = try? AttributedString(markdown: text, options: .init(interpretedSyntax: .full)) {
|
||||
Text(attrString)
|
||||
.font(.system(size: fontSize))
|
||||
.foregroundColor(.oaiPrimary)
|
||||
.lineSpacing(4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
Text(text)
|
||||
.font(.system(size: fontSize))
|
||||
.foregroundColor(.oaiPrimary)
|
||||
.lineSpacing(4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
@@ -52,6 +56,7 @@ struct MarkdownContentView: View {
|
||||
enum Segment {
|
||||
case text(String)
|
||||
case codeBlock(language: String?, code: String)
|
||||
case table(String)
|
||||
}
|
||||
|
||||
private func parseSegments(_ content: String) -> [Segment] {
|
||||
@@ -61,10 +66,17 @@ struct MarkdownContentView: View {
|
||||
var inCodeBlock = false
|
||||
var codeLanguage: String? = nil
|
||||
var codeContent = ""
|
||||
var inTable = false
|
||||
var tableLines: [String] = []
|
||||
|
||||
for line in lines {
|
||||
if !inCodeBlock && line.hasPrefix("```") {
|
||||
// Start of code block
|
||||
if inTable {
|
||||
segments.append(.table(tableLines.joined(separator: "\n")))
|
||||
tableLines = []
|
||||
inTable = false
|
||||
}
|
||||
if !currentText.isEmpty {
|
||||
segments.append(.text(currentText))
|
||||
currentText = ""
|
||||
@@ -75,7 +87,6 @@ struct MarkdownContentView: View {
|
||||
codeContent = ""
|
||||
} else if inCodeBlock && line.hasPrefix("```") {
|
||||
// End of code block
|
||||
// Remove trailing newline from code
|
||||
if codeContent.hasSuffix("\n") {
|
||||
codeContent = String(codeContent.dropLast())
|
||||
}
|
||||
@@ -85,7 +96,25 @@ struct MarkdownContentView: View {
|
||||
codeContent = ""
|
||||
} else if inCodeBlock {
|
||||
codeContent += line + "\n"
|
||||
} else if line.contains("|") && (line.hasPrefix("|") || line.filter({ $0 == "|" }).count >= 2) {
|
||||
// Markdown table line
|
||||
if !inTable {
|
||||
// Start of table
|
||||
if !currentText.isEmpty {
|
||||
segments.append(.text(currentText))
|
||||
currentText = ""
|
||||
}
|
||||
inTable = true
|
||||
}
|
||||
tableLines.append(line)
|
||||
} else {
|
||||
// Regular text
|
||||
if inTable {
|
||||
// End of table
|
||||
segments.append(.table(tableLines.joined(separator: "\n")))
|
||||
tableLines = []
|
||||
inTable = false
|
||||
}
|
||||
currentText += line + "\n"
|
||||
}
|
||||
}
|
||||
@@ -98,9 +127,13 @@ struct MarkdownContentView: View {
|
||||
segments.append(.codeBlock(language: codeLanguage, code: codeContent))
|
||||
}
|
||||
|
||||
// Handle unclosed table
|
||||
if inTable {
|
||||
segments.append(.table(tableLines.joined(separator: "\n")))
|
||||
}
|
||||
|
||||
// Remaining text
|
||||
if !currentText.isEmpty {
|
||||
// Remove trailing newline
|
||||
if currentText.hasSuffix("\n") {
|
||||
currentText = String(currentText.dropLast())
|
||||
}
|
||||
@@ -113,6 +146,136 @@ struct MarkdownContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table View
|
||||
|
||||
struct TableView: View {
|
||||
let content: String
|
||||
let fontSize: Double
|
||||
|
||||
private struct TableData {
|
||||
let headers: [String]
|
||||
let alignments: [TextAlignment]
|
||||
let rows: [[String]]
|
||||
}
|
||||
|
||||
private var tableData: TableData {
|
||||
parseTable(content)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let data = tableData
|
||||
|
||||
guard !data.headers.isEmpty else {
|
||||
return AnyView(EmptyView())
|
||||
}
|
||||
|
||||
return AnyView(
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Headers
|
||||
HStack(spacing: 0) {
|
||||
ForEach(data.headers.indices, id: \.self) { index in
|
||||
if index < data.headers.count {
|
||||
Text(data.headers[index].trimmingCharacters(in: .whitespaces))
|
||||
.font(.system(size: fontSize, weight: .semibold))
|
||||
.foregroundColor(.oaiPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: alignmentFor(
|
||||
index < data.alignments.count ? data.alignments[index] : .leading
|
||||
))
|
||||
.padding(8)
|
||||
.background(Color.oaiSecondary.opacity(0.1))
|
||||
|
||||
if index < data.headers.count - 1 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(Color.oaiSecondary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
|
||||
// Rows
|
||||
ForEach(data.rows.indices, id: \.self) { rowIndex in
|
||||
if rowIndex < data.rows.count {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<data.headers.count, id: \.self) { colIndex in
|
||||
let cellContent = colIndex < data.rows[rowIndex].count ? data.rows[rowIndex][colIndex] : ""
|
||||
let alignment = colIndex < data.alignments.count ? data.alignments[colIndex] : .leading
|
||||
|
||||
Text(cellContent.trimmingCharacters(in: .whitespaces))
|
||||
.font(.system(size: fontSize))
|
||||
.foregroundColor(.oaiPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: alignmentFor(alignment))
|
||||
.padding(8)
|
||||
|
||||
if colIndex < data.headers.count - 1 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(rowIndex % 2 == 0 ? Color.clear : Color.oaiSecondary.opacity(0.05))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(Color.oaiSecondary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.cornerRadius(8)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(Color.oaiSecondary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func alignmentFor(_ textAlignment: TextAlignment) -> Alignment {
|
||||
switch textAlignment {
|
||||
case .leading: return .leading
|
||||
case .center: return .center
|
||||
case .trailing: return .trailing
|
||||
}
|
||||
}
|
||||
|
||||
private func parseTable(_ content: String) -> TableData {
|
||||
let lines = content.components(separatedBy: "\n").filter { !$0.isEmpty }
|
||||
guard lines.count >= 2 else {
|
||||
return TableData(headers: [], alignments: [], rows: [])
|
||||
}
|
||||
|
||||
// Parse headers (first line)
|
||||
let headers = lines[0].components(separatedBy: "|")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
// Parse alignment row (second line with dashes)
|
||||
let alignmentLine = lines[1]
|
||||
let alignments = alignmentLine.components(separatedBy: "|")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
.map { cell -> TextAlignment in
|
||||
let trimmed = cell.trimmingCharacters(in: .whitespaces)
|
||||
if trimmed.hasPrefix(":") && trimmed.hasSuffix(":") {
|
||||
return .center
|
||||
} else if trimmed.hasSuffix(":") {
|
||||
return .trailing
|
||||
} else {
|
||||
return .leading
|
||||
}
|
||||
}
|
||||
|
||||
// Parse data rows (remaining lines)
|
||||
let rows = lines.dropFirst(2).map { line in
|
||||
line.components(separatedBy: "|")
|
||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
return TableData(headers: headers, alignments: alignments, rows: rows)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Code Block View
|
||||
|
||||
struct CodeBlockView: View {
|
||||
|
||||
Reference in New Issue
Block a user