CSV files load as editable markdown tables, save converts back to CSV

This commit is contained in:
jess 2026-04-06 13:26:16 -07:00
parent 977874cd22
commit cca7d78cb3
1 changed files with 127 additions and 6 deletions

View File

@ -241,9 +241,15 @@ class AppState: ObservableObject {
} }
func saveNote() { func saveNote() {
bridge.setText(currentNoteID, text: documentText) let textToSave: String
if currentFileFormat.isCSV {
textToSave = markdownTableToCSV(documentText)
} else {
textToSave = documentText
}
bridge.setText(currentNoteID, text: textToSave)
if let url = currentFileURL { if let url = currentFileURL {
let _ = bridge.saveNote(currentNoteID, path: url.path) try? textToSave.write(to: url, atomically: true, encoding: .utf8)
} }
let _ = bridge.cacheSave(currentNoteID) let _ = bridge.cacheSave(currentNoteID)
modified = false modified = false
@ -251,18 +257,30 @@ class AppState: ObservableObject {
} }
func saveNoteToFile(_ url: URL) { func saveNoteToFile(_ url: URL) {
let _ = bridge.saveNote(currentNoteID, path: url.path) let format = FileFormat.from(filename: url.lastPathComponent)
let textToSave: String
if format.isCSV {
textToSave = markdownTableToCSV(documentText)
} else {
textToSave = documentText
}
try? textToSave.write(to: url, atomically: true, encoding: .utf8)
currentFileURL = url currentFileURL = url
currentFileFormat = FileFormat.from(filename: url.lastPathComponent) currentFileFormat = format
modified = false modified = false
} }
func loadNoteFromFile(_ url: URL) { func loadNoteFromFile(_ url: URL) {
let format = FileFormat.from(filename: url.lastPathComponent)
if let (id, text) = bridge.loadNote(path: url.path) { if let (id, text) = bridge.loadNote(path: url.path) {
currentNoteID = id currentNoteID = id
documentText = text
currentFileURL = url currentFileURL = url
currentFileFormat = FileFormat.from(filename: url.lastPathComponent) currentFileFormat = format
if format.isCSV {
documentText = csvToMarkdownTable(text)
} else {
documentText = text
}
modified = false modified = false
let _ = bridge.cacheSave(id) let _ = bridge.cacheSave(id)
evaluate() evaluate()
@ -270,6 +288,109 @@ class AppState: ObservableObject {
} }
} }
// MARK: - CSV conversion
private func csvToMarkdownTable(_ csv: String) -> String {
let rows = parseCSVRows(csv)
guard let header = rows.first, !header.isEmpty else { return csv }
var lines: [String] = []
lines.append("| " + header.joined(separator: " | ") + " |")
lines.append("| " + header.map { _ in "---" }.joined(separator: " | ") + " |")
for row in rows.dropFirst() {
var cells = row
while cells.count < header.count { cells.append("") }
lines.append("| " + cells.prefix(header.count).joined(separator: " | ") + " |")
}
return lines.joined(separator: "\n")
}
private func markdownTableToCSV(_ markdown: String) -> String {
let lines = markdown.components(separatedBy: "\n").filter { !$0.isEmpty }
var csvRows: [String] = []
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard trimmed.hasPrefix("|") else { continue }
if isTableSeparatorLine(trimmed) { continue }
let cells = extractTableCells(trimmed)
csvRows.append(cells.map { escapeCSVField($0) }.joined(separator: ","))
}
return csvRows.joined(separator: "\n") + "\n"
}
private func parseCSVRows(_ csv: String) -> [[String]] {
var rows: [[String]] = []
var current: [String] = []
var field = ""
var inQuotes = false
let chars = Array(csv)
var i = 0
while i < chars.count {
let ch = chars[i]
if inQuotes {
if ch == "\"" {
if i + 1 < chars.count && chars[i + 1] == "\"" {
field.append("\"")
i += 2
continue
}
inQuotes = false
} else {
field.append(ch)
}
} else {
if ch == "\"" {
inQuotes = true
} else if ch == "," {
current.append(field.trimmingCharacters(in: .whitespaces))
field = ""
} else if ch == "\n" || ch == "\r" {
current.append(field.trimmingCharacters(in: .whitespaces))
field = ""
if !current.isEmpty {
rows.append(current)
}
current = []
if ch == "\r" && i + 1 < chars.count && chars[i + 1] == "\n" {
i += 1
}
} else {
field.append(ch)
}
}
i += 1
}
if !field.isEmpty || !current.isEmpty {
current.append(field.trimmingCharacters(in: .whitespaces))
rows.append(current)
}
return rows
}
private func isTableSeparatorLine(_ line: String) -> Bool {
let stripped = line.replacingOccurrences(of: " ", with: "")
return stripped.allSatisfy { "|:-".contains($0) } && stripped.contains("-")
}
private func extractTableCells(_ line: String) -> [String] {
var trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("|") { trimmed = String(trimmed.dropFirst()) }
if trimmed.hasSuffix("|") { trimmed = String(trimmed.dropLast()) }
return trimmed.components(separatedBy: "|").map { $0.trimmingCharacters(in: .whitespaces) }
}
private func escapeCSVField(_ field: String) -> String {
if field.contains(",") || field.contains("\"") || field.contains("\n") {
return "\"" + field.replacingOccurrences(of: "\"", with: "\"\"") + "\""
}
return field
}
func deleteNote(_ id: UUID) { func deleteNote(_ id: UUID) {
bridge.deleteNote(id) bridge.deleteNote(id)
if id == currentNoteID { if id == currentNoteID {