Skip to content

Instantly share code, notes, and snippets.

@evitiello
Created May 19, 2024 20:59
Show Gist options
  • Save evitiello/7cfe8c31f6b1f59f66dc16f337e1410a to your computer and use it in GitHub Desktop.
Save evitiello/7cfe8c31f6b1f59f66dc16f337e1410a to your computer and use it in GitHub Desktop.
kindle2markdown
#!/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