CSV files load as editable markdown tables, save converts back to CSV
This commit is contained in:
parent
977874cd22
commit
cca7d78cb3
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue