Skip to content
Merged

Dev #22

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ import Foundation
- Auto-detect data types
- Handle special characters and quotes
- Powered by swift-textfile library for robust CSV/TSV parsing and generation
- Falls back to manual parsing/generation for custom separators (when separator ≠ comma or tab)
- CSV and TSV only (no custom delimiters); spec-compliant parsing/generation via swift-textfile

### Image Operations (XLKitImages Module)
- Support GIF, PNG, JPEG formats (BMP, TIFF removed for compatibility)
Expand Down
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@

**🔧 Improvements:**
- Migrated CSV/TSV dependency from [swift-textfile-tools](https://github.com/orchetect/swift-textfile-tools) to [swift-textfile](https://github.com/orchetect/swift-textfile)
- Updated `XLKitFormatters/CSVUtils` to use `import TextFile` and the `TextFile` product; all CSV/TSV behavior unchanged
- Updated documentation and rules (`.cursorrules`, AGENT.MD, README, Manual, tests) to reference swift-textfile and TextFile
- Updated `XLKitFormatters/CSVUtils` to use `import TextFile` and the `TextFile` product
- CSV and TSV are treated as defined formats (e.g. RFC 4180 for CSV) with standard quote/escape rules; all parsing and generation go through swift-textfile for spec compliance
- Removed custom-delimiter boilerplate (`generateCustomDelimitedText`, `parseCustomDelimitedText`) to avoid non-standard formats and delimiter-vs-content collision pitfalls
- Removed custom delimiter support from CSV import/export. Only CSV (comma) and TSV (tab) are now supported.
- **API changes:** `exportToCSV(separator:)` → `exportToCSV()`; `importCSV(_:into:separator:hasHeader:)` → `importCSV(_:into:hasHeader:)`; `init(fromCSV:sheetName:separator:hasHeader:)` → `init(fromCSV:sheetName:hasHeader:)`; `exportSheetToCSV(_:separator:)` → `exportSheetToCSV(_:)`. Same for `CSVUtils` static methods (no `separator` parameter).
- Updated documentation and rules (`.cursorrules`, AGENT.MD, README, Manual) to reference swift-textfile and TextFile

---

Expand Down
32 changes: 16 additions & 16 deletions Documentation/Manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ let range2 = CellRange(excelRange: "A1:B5")
- Sheet: `setCell`, `setRow`, `setColumn`, `setRange`, `mergeCells`, `embedImageAutoSized`, `setColumnWidth`
- Convenience Methods: Type-specific setters like `setCell(string:format:)`, `setRange(number:format:)`
- Images: GIF, PNG, JPEG with perfect aspect ratio preservation
- CSV/TSV: `Workbook(fromCSV:)`, `exportToCSV()`, `importCSVIntoSheet`
- CSV/TSV: `Workbook(fromCSV:)`, `Workbook(fromTSV:)`, `exportToCSV()`, `exportToTSV()`, `importCSV(_:into:hasHeader:)`, `importTSV(_:into:hasHeader:)`
- Fluent API: Most setters return `Self` for chaining
- Bulk Data: `setRow`, `setColumn` for easy import
- Utility Properties: `allCells`, `allFormattedCells`, `isEmpty`, `cellCount`, `imageCount`
Expand Down Expand Up @@ -663,7 +663,7 @@ do {

## CSV/TSV Import & Export

XLKit provides simple static methods for importing and exporting CSV/TSV data. CSV/TSV parsing and generation is powered by the [swift-textfile](https://github.com/orchetect/swift-textfile) library, which provides robust handling of quoted fields, escaped quotes, and edge cases. For custom separators (other than comma or tab), XLKit falls back to manual parsing/generation.
XLKit provides simple static methods for importing and exporting CSV/TSV data. Only CSV (comma) and TSV (tab) are supported; both are defined formats with standard quote and escape rules (e.g. RFC 4180 for CSV). Parsing and generation are powered by the [swift-textfile](https://github.com/orchetect/swift-textfile) library for spec-compliant handling of quoted fields and escaped quotes.

```swift
// Create a workbook from CSV
Expand All @@ -678,8 +678,8 @@ let sheet = workbook.getSheets().first!
// Export a sheet to CSV
let csv = sheet.exportToCSV()

// Import CSV into an existing sheet
sheet.importCSV(csvData, hasHeader: true)
// Import CSV into an existing sheet (Workbook method)
workbook.importCSV(csvData, into: sheet, hasHeader: true)

// Create a workbook from TSV
let tsvData = """
Expand All @@ -693,8 +693,8 @@ let tsvSheet = tsvWorkbook.getSheets().first!
// Export a sheet to TSV
let tsv = tsvSheet.exportToTSV()

// Import TSV into an existing sheet
tsvSheet.importTSV(tsvData, hasHeader: true)
// Import TSV into an existing sheet (Workbook method)
tsvWorkbook.importTSV(tsvData, into: tsvSheet, hasHeader: true)
```

All CSV/TSV helpers are available as instance methods on `Workbook` and `Sheet` classes for convenience, and are powered by the `XLKitFormatters` module under the hood.
Expand Down Expand Up @@ -1411,13 +1411,13 @@ This section lists the full public API of XLKit. All types and members are avail
| `getImages(withFormat format: ImageFormat) -> [ExcelImage]` | Return images filtered by format. |
| `clearImages()` | Remove all workbook-level images. |
| `imageCount: Int` | Number of images in the workbook. |
| `init(fromCSV csvData: String, sheetName:separator:hasHeader:)` | Convenience initializer: create a workbook from CSV. |
| `init(fromCSV csvData: String, sheetName:hasHeader:)` | Convenience initializer: create a workbook from CSV (comma-separated). |
| `init(fromTSV tsvData: String, sheetName:hasHeader:)` | Convenience initializer: create a workbook from TSV. |
| `save(to url: URL) throws` | Save the workbook to a file (synchronous; `@MainActor`). |
| `save(to url: URL) async throws` | Save the workbook to a file (asynchronous; `@MainActor`). |
| `importCSV(_:into:separator:hasHeader:)` | Import CSV into an existing sheet. |
| `importCSV(_:into:hasHeader:)` | Import CSV into an existing sheet (comma-separated). |
| `importTSV(_:into:hasHeader:)` | Import TSV into an existing sheet. |
| `exportSheetToCSV(_ sheet:separator:) -> String` | Export a sheet to CSV. |
| `exportSheetToCSV(_ sheet:) -> String` | Export a sheet to CSV (comma-separated). |
| `exportSheetToTSV(_ sheet:) -> String` | Export a sheet to TSV. |

### Sheet API
Expand Down Expand Up @@ -1489,7 +1489,7 @@ This section lists the full public API of XLKit. All types and members are avail
| `embedImage(_ data:at:of:scale:maxWidth:maxHeight:) async throws -> Bool` | Embed image with scaling parameters. |
| `embedImage(from url: URL, at coordinate: String, displaySize: CGSize? = nil) async throws -> Bool` | Embed image from file URL. |
| `embedImage(from path: String, at coordinate: String, displaySize: CGSize? = nil) async throws -> Bool` | Embed image from file path. |
| `exportToCSV(separator: String = ",") -> String` | Export sheet to CSV. |
| `exportToCSV() -> String` | Export sheet to CSV (comma-separated). |
| `exportToTSV() -> String` | Export sheet to TSV. |

**Other**
Expand Down Expand Up @@ -1635,15 +1635,15 @@ Static configuration and methods (`@MainActor`):

### CSVUtils

Static methods; typically used via `Workbook`/`Sheet` instance methods. CSV/TSV parsing and generation is powered by the [swift-textfile](https://github.com/orchetect/swift-textfile) library for robust handling of quoted fields, escaped quotes, and edge cases.
Static methods; typically used via `Workbook`/`Sheet` instance methods. Only CSV (comma) and TSV (tab) are supported. Parsing and generation are powered by the [swift-textfile](https://github.com/orchetect/swift-textfile) library for spec-compliant handling of quoted fields, escaped quotes, and edge cases (e.g. RFC 4180 for CSV).

| Member | Description |
|--------|-------------|
| `exportToCSV(sheet: Sheet, separator: String = ",") -> String` | Export sheet to CSV. Uses TextFile for comma-separated values; falls back to manual generation for custom separators. |
| `exportToTSV(sheet: Sheet) -> String` | Export sheet to TSV. Uses TextFile for tab-separated values. |
| `importFromCSV(sheet: Sheet, csvData: String, separator: String, hasHeader: Bool)` | Import CSV into sheet. Uses TextFile for comma-separated values; falls back to manual parsing for custom separators. |
| `importFromTSV(sheet: Sheet, tsvData: String, hasHeader: Bool)` | Import TSV into sheet. Uses TextFile for tab-separated values. |
| `createWorkbookFromCSV(csvData: String, sheetName: String, separator: String, hasHeader: Bool) -> Workbook` | New workbook from CSV. |
| `exportToCSV(sheet: Sheet) -> String` | Export sheet to CSV (comma-separated; spec-compliant via TextFile). |
| `exportToTSV(sheet: Sheet) -> String` | Export sheet to TSV (tab-separated; spec-compliant via TextFile). |
| `importFromCSV(sheet: Sheet, csvData: String, hasHeader: Bool)` | Import CSV into sheet (comma-separated). |
| `importFromTSV(sheet: Sheet, tsvData: String, hasHeader: Bool)` | Import TSV into sheet. |
| `createWorkbookFromCSV(csvData: String, sheetName: String, hasHeader: Bool) -> Workbook` | New workbook from CSV (comma-separated). |
| `createWorkbookFromTSV(tsvData: String, sheetName: String, hasHeader: Bool) -> Workbook` | New workbook from TSV. |

### XLKitError
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Version | Supported |
| ------- | ------------------ |
| 1.1.1 | :white_check_mark: |
| 1.1.2 | :white_check_mark: |

## Reporting a Vulnerability

Expand Down
6 changes: 3 additions & 3 deletions Sources/XLKit/Sheet+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ public extension Sheet {
return setColumn(column, values: values)
}
// MARK: - CSV/TSV Export Methods
/// Exports the sheet to CSV format
func exportToCSV(separator: String = ",") -> String {
return CSVUtils.exportToCSV(sheet: self, separator: separator)
/// Exports the sheet to CSV format (comma-separated).
func exportToCSV() -> String {
return CSVUtils.exportToCSV(sheet: self)
}
/// Exports the sheet to TSV format
func exportToTSV() -> String {
Expand Down
18 changes: 9 additions & 9 deletions Sources/XLKit/Workbook+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import XLKitXLSX
import XLKitFormatters

public extension Workbook {
/// Creates a workbook from CSV data
convenience init(fromCSV csvData: String, sheetName: String = "Sheet1", separator: String = ",", hasHeader: Bool = false) {
/// Creates a workbook from CSV data (comma-separated).
convenience init(fromCSV csvData: String, sheetName: String = "Sheet1", hasHeader: Bool = false) {
self.init()
let sheet = addSheet(name: sheetName)
importCSV(csvData, into: sheet, separator: separator, hasHeader: hasHeader)
importCSV(csvData, into: sheet, hasHeader: hasHeader)
}
/// Creates a workbook from TSV data
convenience init(fromTSV tsvData: String, sheetName: String = "Sheet1", hasHeader: Bool = false) {
Expand All @@ -34,17 +34,17 @@ public extension Workbook {
try XLSXEngine.generateXLSX(workbook: self, to: url)
}
// MARK: - CSV/TSV Operations
/// Imports CSV data into a sheet
func importCSV(_ csvData: String, into sheet: Sheet, separator: String = ",", hasHeader: Bool = false) {
CSVUtils.importFromCSV(sheet: sheet, csvData: csvData, separator: separator, hasHeader: hasHeader)
/// Imports CSV data into a sheet (comma-separated).
func importCSV(_ csvData: String, into sheet: Sheet, hasHeader: Bool = false) {
CSVUtils.importFromCSV(sheet: sheet, csvData: csvData, hasHeader: hasHeader)
}
/// Imports TSV data into a sheet
func importTSV(_ tsvData: String, into sheet: Sheet, hasHeader: Bool = false) {
CSVUtils.importFromTSV(sheet: sheet, tsvData: tsvData, hasHeader: hasHeader)
}
/// Exports a sheet to CSV format
func exportSheetToCSV(_ sheet: Sheet, separator: String = ",") -> String {
return CSVUtils.exportToCSV(sheet: sheet, separator: separator)
/// Exports a sheet to CSV format (comma-separated).
func exportSheetToCSV(_ sheet: Sheet) -> String {
return CSVUtils.exportToCSV(sheet: sheet)
}
/// Exports a sheet to TSV format
func exportSheetToTSV(_ sheet: Sheet) -> String {
Expand Down
135 changes: 28 additions & 107 deletions Sources/XLKitFormatters/CSVUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,51 @@ import Foundation
@preconcurrency import XLKitCore
import TextFile

/// CSV/TSV import/export utilities for XLKit
/// Uses swift-textfile library for robust CSV/TSV parsing and generation
/// CSV/TSV import/export utilities for XLKit.
///
/// Only CSV (comma) and TSV (tab) are supported. These are defined formats with
/// standard quote and escape rules (e.g. RFC 4180 for CSV). Custom delimiters
/// are not supported to avoid non-standard formats and delimiter-vs-content
/// collision pitfalls.
///
/// Uses the swift-textfile library for spec-compliant CSV/TSV parsing and generation.
public struct CSVUtils {
/// Exports a sheet to CSV format
public static func exportToCSV(sheet: Sheet, separator: String = ",") -> String {

/// Exports a sheet to CSV format (comma-separated, RFC 4180-style).
public static func exportToCSV(sheet: Sheet) -> String {
// Find max row/column from used cells
let usedCells = sheet.getUsedCells()
var maxRow = 0
var maxColumn = 0

for coordinate in usedCells {
guard let cellCoord = CellCoordinate(excelAddress: coordinate) else { continue }
maxRow = max(maxRow, cellCoord.row)
maxColumn = max(maxColumn, cellCoord.column)
}

// Build StringTable from sheet data
var stringTable: StringTable = []

for row in 1...maxRow {
var rowData: [String] = []

for column in 1...maxColumn {
let coord = CellCoordinate(row: row, column: column)
let cellAddress = coord.excelAddress

if let cellValue = sheet.getCell(cellAddress) {
rowData.append(cellValue.stringValue)
} else {
rowData.append("")
}
}

stringTable.append(rowData)
}

// Use TextFile for CSV generation
if separator == "," {
let csv = CSV(table: stringTable)
return csv.rawText
} else {
// For custom separators, we need to use a delimited format
// Since TextFile only supports CSV (comma) and TSV (tab),
// we'll fall back to manual generation for custom separators
return generateCustomDelimitedText(table: stringTable, separator: separator)
}

let csv = CSV(table: stringTable)
return csv.rawText
}

/// Exports a sheet to TSV format
Expand Down Expand Up @@ -95,18 +93,11 @@ public struct CSVUtils {
return tsv.rawText
}

/// Imports CSV data into a sheet
public static func importFromCSV(sheet: Sheet, csvData: String, separator: String = ",", hasHeader: Bool = false) {
// Use TextFile for CSV parsing
let stringTable: StringTable
if separator == "," {
let csv = CSV(rawText: csvData)
stringTable = csv.table
} else {
// For custom separators, parse manually
stringTable = parseCustomDelimitedText(text: csvData, separator: separator)
}

/// Imports CSV data into a sheet (comma-separated, RFC 4180-style).
public static func importFromCSV(sheet: Sheet, csvData: String, hasHeader: Bool = false) {
let csv = CSV(rawText: csvData)
let stringTable = csv.table

// Determine which rows to import
let dataRows: [[String]]
if hasHeader && !stringTable.isEmpty {
Expand Down Expand Up @@ -161,11 +152,11 @@ public struct CSVUtils {
}
}

/// Creates a workbook from CSV data
public static func createWorkbookFromCSV(csvData: String, sheetName: String = "Sheet1", separator: String = ",", hasHeader: Bool = false) -> Workbook {
/// Creates a workbook from CSV data (comma-separated).
public static func createWorkbookFromCSV(csvData: String, sheetName: String = "Sheet1", hasHeader: Bool = false) -> Workbook {
let workbook = Workbook()
let sheet = workbook.addSheet(name: sheetName)
importFromCSV(sheet: sheet, csvData: csvData, separator: separator, hasHeader: hasHeader)
importFromCSV(sheet: sheet, csvData: csvData, hasHeader: hasHeader)
return workbook
}

Expand Down Expand Up @@ -213,76 +204,6 @@ public struct CSVUtils {
return .string(trimmed)
}

/// Generates custom delimited text for separators other than comma or tab
/// Falls back to manual generation when TextFile doesn't support the separator
private static func generateCustomDelimitedText(table: StringTable, separator: String) -> String {
return table.map { row in
row.map { textString in
var outString = textString

// Escape double-quotes
outString = outString.replacingOccurrences(of: "\"", with: "\"\"")

// Wrap string in double-quotes if it contains separator, quotes, or newlines
if outString.contains(separator) ||
outString.contains("\"") ||
outString.contains("\n") ||
outString.contains("\r") {
outString = "\"\(outString)\""
}

return outString
}
.joined(separator: separator)
}
.joined(separator: "\n")
}

/// Parses custom delimited text for separators other than comma or tab
/// Falls back to manual parsing when TextFile doesn't support the separator
private static func parseCustomDelimitedText(text: String, separator: String) -> StringTable {
let lines = text.components(separatedBy: .newlines)
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }

var result: StringTable = []

for line in lines {
var fields: [String] = []
var currentField = ""
var inQuotes = false
var i = 0

while i < line.count {
let char = line[line.index(line.startIndex, offsetBy: i)]

if char == "\"" {
if inQuotes {
// Check for escaped quote
if i + 1 < line.count && line[line.index(line.startIndex, offsetBy: i + 1)] == "\"" {
currentField += "\""
i += 1 // Skip the next quote
} else {
inQuotes = false
}
} else {
inQuotes = true
}
} else if String(char) == separator && !inQuotes {
fields.append(currentField)
currentField = ""
} else {
currentField += String(char)
}

i += 1
}

fields.append(currentField)
result.append(fields)
}

return result
}
}

// MARK: - String Extension for Pattern Matching
Expand Down
Loading