Created
May 19, 2024 20:59
-
-
Save evitiello/7cfe8c31f6b1f59f66dc16f337e1410a to your computer and use it in GitHub Desktop.
kindle2markdown
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/swift -enable-bare-slash-regex | |
import Foundation | |
// Define the path to the "My Clippings.txt" file | |
let clippingsFilePath = "My Clippings.txt" | |
// Define the path to the output Markdown files | |
let outputMarkdownFilePath = "clippings/" | |
// All of the books. | |
typealias Title = String | |
let books: [Title : Book] = [:] | |
// Function to read the content of the file | |
func readFile(at path: String) -> String? { | |
do { | |
let content = try String(contentsOfFile: path, encoding: .utf8) | |
return content | |
} catch { | |
print("Error reading file: \(error)") | |
return nil | |
} | |
} | |
// Function to write content to a file | |
func writeFile(content: String, to path: String) { | |
do { | |
try content.write(toFile: path, atomically: true, encoding: .utf8) | |
} catch { | |
print("Error writing file: \(error)") | |
} | |
} | |
// Function to parse the clippings and convert to Markdown | |
func convertMyClippingsToArray(clippings: String) -> [Clipping] { | |
var returnValue: [Clipping] = [] | |
let entries = clippings.components(separatedBy: "=\r\n") | |
print("Total entries found: \(entries.count)\n") | |
if (entries.count < 2) { | |
print("Insufficient entries.") | |
exit(255) | |
} | |
for entry in entries { | |
let lines = entry.components(separatedBy: "\r\n").filter { !$0.isEmpty } | |
if lines.count >= 3 { | |
// TODO: Need to clean the title. | |
let regex = /([\w]+)/ | |
var title = "" | |
var author = "" | |
if let match = lines[0].firstMatch(of: regex) { | |
title = String(match.1) | |
//author = String(match.2) | |
} else { | |
title = "NO TITLE" | |
author = "NO AUTHOR" | |
} | |
// TODO: break up the metadata into parts. | |
let metadata = lines[1] | |
let content = lines[2...].joined(separator: "\n") | |
returnValue.append( | |
Clipping(title: title, | |
author: author, | |
metadata: Metadata(from: metadata), | |
content: content) | |
) | |
} | |
} | |
return returnValue | |
} | |
/// Does what the label says. | |
/// - Parameter clippings: an array of `Clipping` | |
/// - Returns: does the thing. | |
func convertClippingsToBooks(clippings: [Clipping]) -> [Book] { | |
var books: [String : Book] = [:] | |
for clipping in clippings { | |
if let val = books[clipping.title] { | |
// append the new highlight | |
var highlights = val.highlights | |
highlights.append(clipping.content) | |
books[clipping.title] = Book(title: val.title, | |
author: val.author, | |
metadata: val.metadata, | |
highlights: highlights) | |
} else { | |
// this is the first time this book has been seen. | |
books[clipping.title] = Book(title: clipping.title, | |
author: clipping.author, | |
metadata: clipping.metadata, | |
highlights: [clipping.content]) | |
} | |
} | |
return books.map{ $1 } | |
} | |
/// A single clipping from the My Clippings file | |
struct Clipping { | |
var title: String | |
var author: String | |
var metadata: Metadata | |
var content: String | |
} | |
/// The metadata... | |
struct Metadata { | |
var page: String | |
var location: String | |
var date: String | |
init(from line: String) { | |
// - Your Highlight on page 129 | Location 2033-2035 | Added on Tuesday, August 16, 2022 11:19:37 PM | |
let parts = line.components(separatedBy: "|") | |
page = parts[0] | |
location = parts[1] | |
date = parts[2] | |
} | |
} | |
/// All of the clippings for a single book. | |
class Book { | |
var title: String | |
var author: String | |
var metadata: Metadata | |
var highlights: [String] | |
/// The content to be written to the file | |
/// - Returns: the content. | |
func writableContent() -> String { | |
var markdownContent = "===\n" | |
markdownContent += "kindle-sync\n" | |
markdownContent += " title: \(self.title)\n" | |
markdownContent += " author: \(self.author)\n" | |
markdownContent += " highlightsCount: \(String(self.highlights.count))\n" | |
markdownContent += "===\n\n" | |
markdownContent += "# \(self.title)\n" | |
markdownContent += "\n## Metadata" | |
markdownContent += " title: \(self.title)\n" | |
markdownContent += " author: \(self.author)\n" | |
markdownContent += " highlightsCount: \(String(self.highlights.count))\n" | |
markdownContent += "\n## Highlights" | |
markdownContent += "\(self.highlights.joined(separator: "\n---\n"))\n\n" | |
return markdownContent | |
} | |
/// Converts the title to a filename | |
/// - Returns: a cleaned string that can be used as the filename | |
func filename() -> String { | |
let regex = try! NSRegularExpression(pattern: "/[^\\W\\w]/", options: []) | |
let range = NSRange(location: 0, length: self.title.utf16.count) | |
let filename = regex.stringByReplacingMatches(in: self.title, | |
options: [], | |
range: range, | |
withTemplate: "_") | |
return "\(filename).md" | |
} | |
init(title: String, author: String, metadata: Metadata, highlights: [String]) { | |
self.title = title | |
self.author = author | |
self.metadata = metadata | |
self.highlights = highlights | |
} | |
} | |
/// Main script execution | |
let count = 0 | |
if let clippingsContent = readFile(at: clippingsFilePath) { | |
let clippings = convertMyClippingsToArray(clippings: clippingsContent) | |
let books = convertClippingsToBooks(clippings: clippings) | |
print("Total Books: \(books.count)") | |
for book in books { | |
writeFile(content: book.writableContent(), | |
to: "\(outputMarkdownFilePath)\(book.filename())") | |
} | |
print("Conversion complete. Markdown file created at \(outputMarkdownFilePath)") | |
} else { | |
print("Failed to read the clippings file.") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment