Skip to content

Commit

Permalink
Merge pull request #2 from iambenzo/ft/output
Browse files Browse the repository at this point in the history
  • Loading branch information
iambenzo authored Nov 2, 2024
2 parents 031b7c2 + 1950d33 commit 794b5c6
Show file tree
Hide file tree
Showing 8 changed files with 554 additions and 37 deletions.
414 changes: 408 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.20", features = ["derive"] }
regex = "1.11.0"
tera = { version = "1.20.0", default-features = false }
serde = { version = "1.0", features = ["derive"] }
5 changes: 5 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
_default:
just -l

# start tmux session
tmux:
tmux new -Pd -s ktr -n code
tmux send-keys -t ktr:1 "vim" Enter
Expand All @@ -22,3 +23,7 @@ cover:
tidy:
cargo fmt
cargo clippy

# remove test artifacts
clean:
rm -r output/
30 changes: 8 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,30 @@ use regex::Regex;
use std::collections::HashMap;
use std::fs::{self};
use std::io::{self};
use std::path::{Path, PathBuf};
use std::path::Path;

use self::model::{Book, Highlight, HighlightLocation, Note};

pub mod model;
pub mod output;

pub fn run(clippings: PathBuf, _template: Option<PathBuf>) {
if let Ok(s) = read_file_string(clippings) {
let books = parse_clippings(s);

for (_, book) in books.iter() {
println!(
"{} by {} has {} highlights",
book.title(),
book.author(),
book.highlights().len()
);
}
} else {
panic!("error reading file");
}
}

fn read_file_string<P>(filename: P) -> io::Result<Vec<String>>
/// read a clippings file and return it as a [Vec<String>]
pub fn read_file_string<P>(filename: P) -> io::Result<Vec<String>>
where
P: AsRef<Path>,
{
Ok(fs::read_to_string(filename)?
.replace("\r\n", " ") // clean line endings
.replace("\n", " ")
.replace("\u{feff}", "") // clean the BOM
.split("==========")
.map(String::from)
.collect())
}

fn parse_clippings(clippings: Vec<String>) -> HashMap<String, Book> {
/// using a [Vec<String>] as the input, return a [HashMap<String, Book>] where the [String]
/// represents the book's title
pub fn parse_clippings(clippings: Vec<String>) -> HashMap<String, Book> {
let mut library: HashMap<String, Book> = HashMap::new();

let re_highlights = Regex::new(r"\s*(?<title>.*)\s\((?<author>.*,.*)\) - Your Highlight on page (?<page>\d+) \| location (?<loc_start>\d+)-(?<loc_end>\d+) \| Added on (?<timestamp>.+ \d{4} \d{2}:\d{2}:\d{2})\s+(?<quote>.*)\s*").unwrap();
Expand Down Expand Up @@ -79,7 +67,6 @@ fn parse_clippings(clippings: Vec<String>) -> HashMap<String, Book> {
}
}

println!("library length: {}", library.len());
library
}

Expand Down Expand Up @@ -128,7 +115,6 @@ Smidgen the pigeon
.map(String::from)
.collect();

dbg!(&input);
input
}

Expand Down
52 changes: 49 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use clap::Parser;
use kindle_clippings::run;
use std::path::PathBuf;
use kindle_clippings::output::render_output;
use kindle_clippings::{parse_clippings, read_file_string};
use std::env;
use std::fs::create_dir;
use std::path::{Path, PathBuf};

#[derive(Parser)]
#[command(version, about, long_about)]
Expand All @@ -10,11 +13,54 @@ struct Cli {

#[arg(short, long, value_name = "TEMPLATE_FILE")]
template: Option<PathBuf>,

#[arg(short, long, value_name = "OUTPUT_DIR")]
output: Option<PathBuf>,
}

fn main() {
let cli = Cli::parse();
run(cli.file, cli.template);

// create/validate provided output directory
if let Some(o) = cli.output {
// create directory if it doesn't exist
if !o.exists() {
if let Err(e) = create_dir(&o) {
eprintln!("Unable to create output directory: {}", e);
::std::process::exit(1);
}

// check provided dir is actually a directory
} else if !o.is_dir() {
eprintln!("{} is not a directory!", o.display());
::std::process::exit(1);
}
run(cli.file, cli.template, &o);

// default to "output" directory
} else {
let mut pwd = env::current_dir().unwrap();
pwd.push("output");

if let Err(e) = create_dir(&pwd) {
eprintln!("Unable to create output directory: {}", e);
::std::process::exit(1);
}

run(cli.file, cli.template, &pwd);
}
}

pub fn run(clippings: PathBuf, template: Option<PathBuf>, output_dir: &Path) {
if let Ok(s) = read_file_string(clippings) {
let books = parse_clippings(s);

for (_, book) in books.iter() {
render_output(book, &template, output_dir);
}
} else {
panic!("error reading file");
}
}

#[test]
Expand Down
18 changes: 12 additions & 6 deletions src/model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::BTreeMap;

#[derive(Debug, Clone)]
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
pub struct Note {
page: u64,
location: u64,
Expand Down Expand Up @@ -29,7 +31,7 @@ impl Note {
}
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct HighlightLocation(u64, u64);

impl HighlightLocation {
Expand All @@ -44,11 +46,11 @@ impl HighlightLocation {
}
}

#[derive(Debug)]
#[derive(Debug, Clone, Serialize)]
pub struct Highlight {
page: u64,
location: HighlightLocation,
quote: Option<String>,
quote: String,
note: Option<Note>,
}

Expand All @@ -57,7 +59,7 @@ impl Highlight {
Highlight {
page,
location,
quote: Some(quote),
quote,
note: None,
}
}
Expand All @@ -75,7 +77,7 @@ impl Highlight {
}

pub fn add_quote(&mut self, quote: String) {
self.quote = Some(quote);
self.quote = quote;
}

fn add_note(&mut self, note: Note) {
Expand Down Expand Up @@ -111,6 +113,10 @@ impl Book {
&self.highlights
}

pub fn quotes(&self) -> Vec<String> {
self.highlights.values().map(|h| h.quote.clone()).collect()
}

pub fn add_highlight(&mut self, highlight: Highlight) {
self.highlights
.insert(highlight.location().to_owned(), highlight);
Expand Down
54 changes: 54 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::fs::File;
use std::path::{Path, PathBuf};

use tera::{Context, Tera};

use crate::model::{Book, Highlight};

/// Uses a [Book] and optionally a [PathBuf] to a custom template file to render the highlights and
/// notes captured whilst reading to a file in the output [Path].
///
/// Will use a default template if a custom template isn't provided.
pub fn render_output(book: &Book, template: &Option<PathBuf>, output_dir: &Path) {
let mut tera = Tera::default();
tera.add_raw_template("default", include_str!("templates/default.md"))
.unwrap();

let mut ctx = Context::new();
ctx.insert(
"highlights",
&book
.highlights()
.values()
.cloned()
.collect::<Vec<Highlight>>(),
);
ctx.insert("quotes", &book.quotes());

let output: tera::Result<()>;
let mut file_path = output_dir.to_path_buf();
file_path.push(format!("{}. {}.md", book.author(), book.title()));

let file: File = match File::create(file_path.as_path()) {
Ok(f) => f,
Err(e) => {
eprintln!("Error creating output file: {}", e);
// dbg!(e);
::std::process::exit(1);
}
};

// use default template unless user has provided one
if let Some(t) = template {
tera.add_template_file(t, Some("user")).unwrap();
output = tera.render_to("user", &ctx, file);
} else {
output = tera.render_to("default", &ctx, file);
}

if let Err(e) = output {
eprintln!("Parsing error(s): {}", e);
// dbg!(e);
::std::process::exit(1);
}
}
16 changes: 16 additions & 0 deletions src/templates/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
tags:
created:
"{ date }":
type: kindle
---

| Page | Description | Theme |
| ---- | ----------- | ----- |
{%- for hl in highlights %}
| {{ hl.page }} | {{ hl.note.content | default (value="") }} [^{{ loop.index }}]| |
{%- endfor %}

{% for quote in quotes %}
[^{{ loop.index }}]: {{ quote }}
{% endfor %}

0 comments on commit 794b5c6

Please sign in to comment.