diff --git a/src/AppState.swift b/src/AppState.swift index 12ae056..c676824 100644 --- a/src/AppState.swift +++ b/src/AppState.swift @@ -241,9 +241,15 @@ class AppState: ObservableObject { } 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 { - let _ = bridge.saveNote(currentNoteID, path: url.path) + try? textToSave.write(to: url, atomically: true, encoding: .utf8) } let _ = bridge.cacheSave(currentNoteID) modified = false @@ -251,18 +257,30 @@ class AppState: ObservableObject { } 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 - currentFileFormat = FileFormat.from(filename: url.lastPathComponent) + currentFileFormat = format modified = false } func loadNoteFromFile(_ url: URL) { + let format = FileFormat.from(filename: url.lastPathComponent) if let (id, text) = bridge.loadNote(path: url.path) { currentNoteID = id - documentText = text currentFileURL = url - currentFileFormat = FileFormat.from(filename: url.lastPathComponent) + currentFileFormat = format + if format.isCSV { + documentText = csvToMarkdownTable(text) + } else { + documentText = text + } modified = false let _ = bridge.cacheSave(id) 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) { bridge.deleteNote(id) if id == currentNoteID {