bridgetown_readwise_curator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f75f4f7b727786719b11287d1276acfa992e5a97ca27bc300ee33c52a69ceeb
4
+ data.tar.gz: 686a479fb624a7c889d09dbe42012d43007cbff687cc4c673a613701fff15496
5
+ SHA512:
6
+ metadata.gz: 9ec0c6d400898efd5f992f81d130656e79dd4d2a81862edea6fc57516f05eeb900ac15267182142a2a9eafa5815a2e6d79441e332de0505e9782fb5604f06c3f
7
+ data.tar.gz: 44d930380bd8405b609912cef9fd4e138b035a52f9ffc0c17e0c343e18102e389f1c89a7b1c911d7c436eeb7208f2f2c206e7a5375cb0d25a63d687423563e10
@@ -0,0 +1,32 @@
1
+ name: Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - "*"
7
+ push:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ ruby_version: [2.7.7, 3.0.5, 3.1.3, 3.2.0]
17
+ bridgetown_version: [1.2.0]
18
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
19
+ # Has to be top level to cache properly
20
+ env:
21
+ BUNDLE_JOBS: 3
22
+ BUNDLE_PATH: "vendor/bundle"
23
+ BRIDGETOWN_VERSION: ${{ matrix.bridgetown_version }}
24
+ steps:
25
+ - uses: actions/checkout@master
26
+ - name: Setup Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby_version }}
30
+ bundler-cache: true
31
+ - name: Test with Rake
32
+ run: script/cibuild
data/.gitignore ADDED
@@ -0,0 +1,40 @@
1
+ /vendor
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
16
+ *.gem
17
+ Gemfile.lock
18
+ .bundle
19
+ .ruby-version
20
+
21
+ # Node
22
+ node_modules
23
+ .npm
24
+ .node_repl_history
25
+
26
+ # Yarn
27
+ yarn-error.log
28
+ yarn-debug.log*
29
+ .pnp/
30
+ .pnp.js
31
+
32
+ # Yarn Integrity file
33
+ .yarn-integrity
34
+
35
+ test/dest
36
+ .bridgetown-metadata
37
+ .bridgetown-cache
38
+ .bridgetown-webpack
39
+
40
+ .env
data/.rubocop.yml ADDED
@@ -0,0 +1,23 @@
1
+ require: rubocop-bridgetown
2
+
3
+ inherit_gem:
4
+ rubocop-bridgetown: .rubocop.yml
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 2.7
8
+
9
+ Exclude:
10
+ - .gitignore
11
+ - .rubocop.yml
12
+ - "*.gemspec"
13
+
14
+ - Gemfile.lock
15
+ - CHANGELOG.md
16
+ - LICENSE.txt
17
+ - README.md
18
+ - Rakefile
19
+ - bridgetown.automation.rb
20
+
21
+ - script/**/*
22
+ - test/fixtures/**/*
23
+ - vendor/**/*
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-08-30
11
+
12
+ ### Added
13
+ - Interactive CLI for curating Readwise highlights with pagination
14
+ - Bridgetown plugin integration with `bin/bt readwise` command
15
+ - Highlight fetching from Readwise API with book selection
16
+ - Multi-select highlight curation with preview
17
+ - JSON data file generation in `src/_data/readwise/curated_highlights/`
18
+ - Book collection integration for Bridgetown sites
19
+ - Comprehensive test suite with 27 tests covering CLI, pagination, API integration, and error handling
20
+ - Automation script for easy plugin installation
21
+ - Support for environment variable and site config API key configuration
22
+
23
+ ### Features
24
+ - **CLI Interface**: Thor-based command-line interface with intuitive menus
25
+ - **Pagination**: Smart pagination for large book collections (10 items per page)
26
+ - **Highlight Selection**: Multi-select interface with preview and filtering
27
+ - **Data Integration**: Seamless integration with Bridgetown's data layer
28
+ - **Error Handling**: Robust error handling for API failures and missing configuration
29
+ - **Testing**: Full test coverage including unit, integration, and error handling tests
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gemspec
5
+
6
+ gem "bridgetown", ENV["BRIDGETOWN_VERSION"] if ENV["BRIDGETOWN_VERSION"]
7
+
8
+ group :test do
9
+ gem "minitest"
10
+ gem "minitest-profile"
11
+ gem "minitest-reporters"
12
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020-present
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Bridgetown Readwise Curator
2
+
3
+ A Bridgetown plugin to curate and manage your Readwise highlights directly within your Bridgetown site.
4
+
5
+ ## Installation
6
+
7
+ ### Recommended: Automated Setup
8
+
9
+ Run the automation script for guided setup that handles everything automatically:
10
+
11
+ ```shell
12
+ bin/bridgetown apply https://github.com/pablojimeno/bridgetown_readwise_curator
13
+ ```
14
+
15
+ This will:
16
+ - Add the gem to your Gemfile
17
+ - Install dotenv for environment variable management
18
+ - Prompt for your Readwise API token
19
+ - Set up all necessary configuration files
20
+ - Create required directories
21
+
22
+ ### Manual Installation
23
+
24
+ Alternatively, you can install manually:
25
+
26
+ 1. Add the gem to your Gemfile:
27
+ ```shell
28
+ bundle add bridgetown_readwise_curator
29
+ ```
30
+
31
+ 2. Set up environment variables:
32
+
33
+ Ensure `READWISE_TOKEN` is available in your environment using your preferred method (figaro, rails credentials, etc.)
34
+
35
+ For example, using dotenv:
36
+ ```shell
37
+ bundle add dotenv
38
+ ```
39
+ Add your [Readwise API token](https://readwise.io/access_token) to `.env`:
40
+ ```
41
+ READWISE_TOKEN=your_readwise_api_token_here
42
+ ```
43
+ Add dotenv initializer to `config/initializers.rb`:
44
+ ```ruby
45
+ init :dotenv
46
+ ```
47
+
48
+ 3. Add to `config/boot.rb`:
49
+ ```ruby
50
+ require "bridgetown_readwise_curator"
51
+ ```
52
+
53
+ 4. Add the gem initializer to `config/initializers.rb`:
54
+ ```ruby
55
+ init :bridgetown_readwise_curator
56
+ ```
57
+
58
+ 5. Create required directories:
59
+ ```shell
60
+ mkdir -p src/_data/readwise
61
+ mkdir -p src/_books
62
+ ```
63
+
64
+ 6. Run bundle install:
65
+ ```shell
66
+ bundle install
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ### CLI Commands
72
+
73
+ The plugin provides an interactive CLI accessible via:
74
+
75
+ ```shell
76
+ bin/bt readwise
77
+ ```
78
+
79
+ This launches an interactive menu with options to:
80
+ - **Browse resources**: Paginated view of your Readwise library
81
+ - **Search resources**: Find books by title or author
82
+ - **Update Readwise data**: Sync latest data from Readwise API
83
+ - **Quit**: Exit the CLI
84
+
85
+ ### Features
86
+
87
+ - Interactive CLI for browsing and curating Readwise highlights
88
+ - Search functionality across your book library
89
+ - Pagination for large libraries
90
+ - Data synchronization with Readwise API
91
+ - JSON file generation of selected highlights
92
+ - Book page creation for curated highlights
93
+
94
+ ### Workflow
95
+
96
+ 1. Run `bin/bt readwise` to start the CLI
97
+ 2. Choose "Update Readwise data" to sync your library
98
+ 3. Browse or search for books you want to curate
99
+ 4. Select highlights from the chosen book
100
+ 5. The plugin generates markdown files in `src/_books/` with your curated highlights
101
+
102
+ ## Testing
103
+
104
+ * Run `bundle exec rake test` to run the test suite
105
+ * Or run `script/cibuild` to validate with Rubocop and Minitest together.
106
+
107
+ ## Contributing
108
+
109
+ 1. Fork it (https://github.com/pablojimeno/bridgetown_readwise_curator/fork)
110
+ 2. Clone the fork using `git clone` to your local development machine.
111
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 5. Push to the branch (`git push origin my-new-feature`)
114
+ 6. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/test_*.rb"]
8
+ t.warning = false
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,72 @@
1
+ # Bridgetown Readwise Curator Plugin Setup Automation
2
+ say_status :readwise, "Setting up Bridgetown Readwise Curator plugin..."
3
+
4
+ # Add the gem to Gemfile
5
+ add_gem "bridgetown_readwise_curator"
6
+
7
+ # Prompt for Readwise API token
8
+ api_token = ask "Please enter your Readwise API token (get it from https://readwise.io/access_token):"
9
+
10
+ # Create or update .env file with the API token
11
+ create_file ".env" do
12
+ if File.exist?(".env")
13
+ existing_content = File.read(".env")
14
+ if existing_content.include?("READWISE_TOKEN")
15
+ # Update existing token
16
+ updated_content = existing_content.gsub(/READWISE_TOKEN=.*/, "READWISE_TOKEN=#{api_token}")
17
+ updated_content
18
+ else
19
+ # Add token to existing file
20
+ "#{existing_content.chomp}\nREADWISE_TOKEN=#{api_token}\n"
21
+ end
22
+ else
23
+ # Create new .env file
24
+ "READWISE_TOKEN=#{api_token}\n"
25
+ end
26
+ end
27
+
28
+
29
+ # Add the readwise curator initializer
30
+ add_initializer :"bridgetown_readwise_curator" do
31
+ <<~RUBY
32
+ # Bridgetown Readwise Curator configuration
33
+ # The API key will be automatically loaded from ENV['READWISE_TOKEN']
34
+ RUBY
35
+ end
36
+
37
+ # Add gem require to config/boot.rb
38
+ create_file "config/boot.rb" do
39
+ if File.exist?("config/boot.rb")
40
+ existing_content = File.read("config/boot.rb")
41
+ unless existing_content.include?("bridgetown_readwise_curator")
42
+ "#{existing_content.chomp}\nrequire \"bridgetown_readwise_curator\"\n"
43
+ else
44
+ existing_content
45
+ end
46
+ else
47
+ "# config/boot.rb\nrequire \"bridgetown_readwise_curator\"\n"
48
+ end
49
+ end
50
+
51
+ # Ensure .env is in .gitignore
52
+ gitignore_content = File.exist?(".gitignore") ? File.read(".gitignore") : ""
53
+ unless gitignore_content.include?(".env")
54
+ append_to_file ".gitignore", "\n# Environment variables\n.env\n"
55
+ end
56
+
57
+ # Create data directory for Readwise data
58
+ empty_directory "src/_data/readwise"
59
+
60
+ # Create books collection directory
61
+ empty_directory "src/_books"
62
+
63
+ say_status :readwise, "Plugin setup complete!"
64
+ say_status :readwise, "Run 'bundle install' to install dependencies"
65
+ say_status :readwise, ""
66
+ say_status :readwise, "IMPORTANT: Set up environment variables for READWISE_TOKEN"
67
+ say_status :readwise, "The .env file has been created, but you may need to:"
68
+ say_status :readwise, "1. Add 'dotenv' gem if you want to use .env files"
69
+ say_status :readwise, "2. Add 'init :dotenv' to config/initializers.rb"
70
+ say_status :readwise, "3. Or use your preferred environment management solution"
71
+ say_status :readwise, ""
72
+ say_status :readwise, "You can now run 'bin/bt readwise' to start curating highlights"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/readwise_curator/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "bridgetown_readwise_curator"
7
+ spec.version = ReadwiseCurator::VERSION
8
+ spec.author = "Pablo Jimeno"
9
+ spec.email = "hello@pablojimeno.com"
10
+ spec.summary = "Curate Readwise highlights for Bridgetown sites"
11
+ spec.description = "A Bridgetown plugin that provides an interactive CLI to curate and manage Readwise highlights directly within your Bridgetown site."
12
+ spec.homepage = "https://github.com/pablojimeno/bridgetown_readwise_curator"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r!^(test|script|spec|features|frontend)/!) }
16
+ spec.test_files = spec.files.grep(%r!^test/!)
17
+ spec.require_paths = ["lib"]
18
+ spec.metadata = {
19
+ "bridgetown_plugin" => "true",
20
+ "source_code_uri" => spec.homepage,
21
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
22
+ "changelog_uri" => "#{spec.homepage}/releases",
23
+ "homepage_uri" => spec.homepage
24
+ }
25
+
26
+ spec.required_ruby_version = ">= 2.7.0"
27
+
28
+ spec.add_dependency "bridgetown", ">= 2.0.0.beta5", "< 3.0"
29
+ spec.add_dependency "tty-prompt", "~> 0.23"
30
+
31
+ spec.add_development_dependency "bundler"
32
+ spec.add_development_dependency "rake", ">= 13.0"
33
+ spec.add_development_dependency "rubocop-bridgetown", "~> 0.3"
34
+ end
@@ -0,0 +1 @@
1
+ <p>Just use <code>layout: readwise_curator/layout</code> in your page's front matter.</p>
@@ -0,0 +1,17 @@
1
+ # rubocop:disable all
2
+ module ReadwiseCurator
3
+ class PluginComponent < Bridgetown::Component
4
+ def initialize(hi:)
5
+ @hi = hi
6
+ end
7
+
8
+ # You can remove this and add an ERB, Serbea, etc. template file…or you can
9
+ # use something like Phlex if you're feeling adventurous!
10
+ def template
11
+ <<~HTML
12
+ Well hello there #{hi}!
13
+ HTML
14
+ end
15
+ end
16
+ end
17
+ # rubocop:enable all
@@ -0,0 +1,8 @@
1
+ ---
2
+ layout: default
3
+ title: Example Page
4
+ ---
5
+
6
+ If all goes well, this page will be accessible as [`{{ resource.data.relative_url }}`]({{ resource.data.relative_url }}), and you will see a photo of a train:
7
+
8
+ ![Train on Rails]({{ "/readwise_curator/train-on-rails.jpeg" | relative_url }})
@@ -0,0 +1,9 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ <p>You can include this layout in your site! 🥳</p>
6
+
7
+ {% render "readwise_curator/layout_help" %}
8
+
9
+ {{ content }}
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bridgetown"
4
+
5
+ # Load core components
6
+ require_relative "readwise_curator/version"
7
+ require_relative "readwise_curator/cli_helpers"
8
+ require_relative "readwise_curator/builder"
9
+ require_relative "readwise_curator/highlight_service"
10
+ require_relative "readwise_curator/cli"
11
+
12
+ # Register CLI command
13
+ Bridgetown::Commands::Registrations.register do
14
+ desc "readwise", "Interactively curate Readwise highlights"
15
+ define_method :readwise do
16
+ ReadwiseCurator::CLI.start(["curate"])
17
+ end
18
+ end
19
+
20
+ # Configuration initializer
21
+ Bridgetown.initializer :bridgetown_readwise_curator do |config|
22
+ config.readwise_curator ||= {}
23
+ config.readwise_curator.api_key ||= ENV.fetch("READWISE_TOKEN", nil)
24
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module ReadwiseCurator
7
+ class Builder < Bridgetown::Builder
8
+ priority :low # Run after config load, but early
9
+
10
+ def build
11
+ api_key = config.readwise_curator&.api_key || ENV.fetch("READWISE_TOKEN", nil)
12
+ raise "Readwise API key not configured" unless api_key
13
+
14
+ all_books = fetch_all_books(api_key)
15
+ books_data = JSON.pretty_generate({
16
+ "count" => all_books.length,
17
+ "next" => nil,
18
+ "previous" => nil,
19
+ "results" => all_books,
20
+ })
21
+
22
+ # Write consolidated response to _data/readwise/books.json
23
+ data_dir = File.join(site.root_dir, "src/_data/readwise")
24
+ FileUtils.mkdir_p(data_dir)
25
+ File.write(File.join(data_dir, "books.json"), books_data)
26
+ end
27
+
28
+ private
29
+
30
+ def fetch_all_books(api_key)
31
+ all_books = []
32
+ next_url = "https://readwise.io/api/v2/books/"
33
+
34
+ while next_url
35
+ response = get(next_url, headers: { "Authorization" => "Token #{api_key}" }) do |data|
36
+ data
37
+ end
38
+
39
+ all_books.concat(response["results"])
40
+ next_url = response["next"]
41
+ end
42
+
43
+ all_books
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "tty-prompt"
5
+ require "tty-reader"
6
+ require "json"
7
+ require_relative "cli_helpers"
8
+
9
+ # Environment variables are expected to be loaded by the host application
10
+
11
+ module ReadwiseCurator
12
+ class CLI < Thor
13
+ include CLIHelpers
14
+
15
+ def self.exit_on_failure?
16
+ true
17
+ end
18
+
19
+ desc "curate", "Interactively curate Readwise highlights"
20
+ default_task :curate
21
+
22
+ def curate
23
+ prompt = TTY::Prompt.new
24
+ site = setup_site
25
+
26
+ loop do
27
+ action = prompt_for_action
28
+
29
+ case action
30
+ when :browse
31
+ browse_resources(prompt, site)
32
+ when :search
33
+ search_resources(prompt, site)
34
+ when :update
35
+ update_readwise_data(site)
36
+ when :quit
37
+ Bridgetown.logger.info "Goodbye!"
38
+ exit
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def setup_site
46
+ site = Bridgetown::Site.new(Bridgetown.configuration)
47
+ site.read
48
+ site
49
+ end
50
+
51
+ def prompt_for_action
52
+ TTY::Prompt.new.select("What would you like to do?", [
53
+ { name: "» Browse resources", value: :browse },
54
+ { name: "» Search resources", value: :search },
55
+ { name: "» Update Readwise data", value: :update },
56
+ { name: "» Quit", value: :quit },
57
+ ])
58
+ end
59
+
60
+ def update_readwise_data(site)
61
+ builder = ReadwiseCurator::Builder.new(site)
62
+ builder.build
63
+ Bridgetown.logger.info "Books data updated."
64
+ end
65
+
66
+ def browse_resources(prompt, site)
67
+ service = create_highlight_service(site)
68
+ choices = build_book_choices(site)
69
+
70
+ loop do
71
+ book_id = select_book_with_pagination(prompt, choices)
72
+
73
+ # Handle special return values
74
+ return if book_id == :back_to_menu
75
+
76
+ highlights = fetch_book_highlights(service, book_id)
77
+
78
+ if highlights.empty?
79
+ handle_empty_highlights
80
+ prompt.keypress("Press any key to continue...")
81
+ next
82
+ end
83
+
84
+ selected_highlight_ids = select_highlights(prompt, service, book_id, highlights)
85
+
86
+ next if selected_highlight_ids.nil?
87
+
88
+ save_selected_highlights(service, book_id, highlights, selected_highlight_ids)
89
+ return
90
+ end
91
+ end
92
+
93
+ def search_resources(prompt, site)
94
+ service = create_highlight_service(site)
95
+ choices = build_book_choices(site)
96
+ search_result = search_books(prompt, choices)
97
+ return unless search_result.is_a?(Integer)
98
+
99
+ highlights = fetch_book_highlights(service, search_result)
100
+
101
+ if highlights.empty?
102
+ handle_empty_highlights
103
+ return
104
+ end
105
+
106
+ selected_highlight_ids = select_highlights(prompt, service, search_result, highlights)
107
+ return if selected_highlight_ids.nil?
108
+
109
+ save_selected_highlights(service, search_result, highlights, selected_highlight_ids)
110
+ end
111
+
112
+ def select_book_with_pagination(_prompt, choices)
113
+ reader = TTY::Reader.new
114
+ paginator = BookPaginator.new(choices)
115
+
116
+ loop do
117
+ display_pagination_page(paginator, choices)
118
+ key = reader.read_keypress(nonblock: false)
119
+ result = handle_pagination_input(key, paginator)
120
+ return result if result
121
+ end
122
+ end
123
+
124
+ def display_pagination_page(paginator, choices)
125
+ clear_screen
126
+ show_pagination_header(
127
+ choices, paginator.current_page, paginator.total_pages, paginator.page_size
128
+ )
129
+ display_page_items(paginator)
130
+ display_pagination_instructions
131
+ end
132
+
133
+ def display_page_items(paginator)
134
+ page_items = paginator.send(:page_choices)
135
+ page_items.each_with_index do |item, index|
136
+ Bridgetown.logger.info "[#{index}] #{item[:name]}"
137
+ end
138
+ end
139
+
140
+ def display_pagination_instructions
141
+ Bridgetown.logger.info "\nPress: [0-9] to select resource, [j/k] to paginate, " \
142
+ "[↩︎ Enter] to go back to the menu"
143
+ end
144
+
145
+ def handle_pagination_input(key, paginator)
146
+ case key
147
+ when "j"
148
+ handle_next_page(paginator)
149
+ when "k"
150
+ handle_prev_page(paginator)
151
+ when %r{^[0-9]$}
152
+ handle_number_selection(key, paginator)
153
+ when "\r", "\n"
154
+ :back_to_menu
155
+ when "q"
156
+ exit
157
+ end
158
+ end
159
+
160
+ def handle_next_page(paginator)
161
+ return unless paginator.current_page < paginator.total_pages - 1
162
+
163
+ paginator.handle_selection(:next_page)
164
+ end
165
+
166
+ def handle_prev_page(paginator)
167
+ return unless paginator.current_page.positive?
168
+
169
+ paginator.handle_selection(:prev_page)
170
+ end
171
+
172
+ def handle_number_selection(key, paginator)
173
+ idx = key.to_i
174
+ page_items = paginator.send(:page_choices)
175
+ selected_item = page_items[idx]
176
+ selected_item[:value] if selected_item
177
+ end
178
+
179
+ class BookPaginator
180
+ attr_reader :choices, :current_page, :page_size
181
+
182
+ def initialize(choices, page_size = 10)
183
+ @choices = choices || []
184
+ @page_size = page_size
185
+ @current_page = 0
186
+ end
187
+
188
+ def total_pages
189
+ return 1 if @choices.empty?
190
+
191
+ (@choices.length.to_f / @page_size).ceil
192
+ end
193
+
194
+ def handle_selection(selection)
195
+ case selection
196
+ when :prev_page
197
+ @current_page = [@current_page - 1, 0].max
198
+ when :next_page
199
+ @current_page = [@current_page + 1, total_pages - 1].min
200
+ else
201
+ return selection # Book selection
202
+ end
203
+ nil # Continue pagination
204
+ end
205
+
206
+ private
207
+
208
+ def page_choices
209
+ return [] if @choices.empty?
210
+
211
+ start_idx = @current_page * @page_size
212
+ end_idx = [start_idx + @page_size - 1, @choices.length - 1].min
213
+ @choices[start_idx..end_idx] || []
214
+ end
215
+ end
216
+
217
+ def search_books(prompt, choices)
218
+ search_term = prompt.ask("Enter search term (title or author):") do |q|
219
+ q.required(true)
220
+ end
221
+
222
+ filtered_choices = filter_choices_by_search_term(choices, search_term)
223
+
224
+ if filtered_choices.empty?
225
+ handle_empty_search_results(prompt, choices, search_term)
226
+ else
227
+ display_search_results(prompt, choices, filtered_choices, search_term)
228
+ end
229
+ end
230
+
231
+ def filter_choices_by_search_term(choices, search_term)
232
+ choices.select do |choice|
233
+ matches_title_or_author?(choice, search_term)
234
+ end
235
+ end
236
+
237
+ def matches_title_or_author?(choice, search_term)
238
+ matches_title?(choice, search_term) || matches_author?(choice, search_term)
239
+ end
240
+
241
+ def matches_title?(choice, search_term)
242
+ choice[:title]&.downcase&.include?(search_term.downcase) || false
243
+ end
244
+
245
+ def matches_author?(choice, search_term)
246
+ choice[:author]&.downcase&.include?(search_term.downcase) || false
247
+ end
248
+
249
+ def handle_empty_search_results(prompt, _choices, search_term)
250
+ Bridgetown.logger.info "No books found matching '#{search_term}'"
251
+ prompt.keypress("Press any key to continue...")
252
+ # Return nil to go back to main menu instead of browse resources
253
+ nil
254
+ end
255
+
256
+ def display_search_results(prompt, choices, filtered_choices, search_term)
257
+ clear_screen
258
+ Bridgetown.logger.info "\n🔍 Search results for '#{search_term}' " \
259
+ "(#{filtered_choices.length} found)\n\n"
260
+
261
+ search_choices = filtered_choices + [{ name: "← Back to full list", value: :back }]
262
+ selection = prompt.select("Select a book:", search_choices)
263
+
264
+ selection == :back ? select_book_with_pagination(prompt, choices) : selection
265
+ end
266
+
267
+ def create_highlight_service(site)
268
+ ReadwiseCurator::HighlightService.new(site)
269
+ end
270
+
271
+ def fetch_book_highlights(service, book_id)
272
+ service.fetch_highlights_for_book(book_id)
273
+ end
274
+
275
+ def build_book_choices(site)
276
+ books = site.data.dig("readwise", "books", "results") || []
277
+ books.map do |book|
278
+ slug = book["slug"]
279
+ has_page = File.exist?(File.join(site.root_dir, "src/_books/#{slug}.md"))
280
+
281
+ category_icon = get_category_icon(book["category"])
282
+ page_indicator = has_page ? "* " : ""
283
+
284
+ label = "#{category_icon} #{page_indicator}#{book["title"]} by #{book["author"]}"
285
+ { name: label, value: book["id"], title: book["title"], author: book["author"] }
286
+ end
287
+ end
288
+
289
+ def select_highlights(prompt, service, book_id, highlights)
290
+ selected_ids = load_existing_selections(service, book_id)
291
+ highlight_choices = build_highlight_choices(highlights, selected_ids)
292
+
293
+ Bridgetown.logger.info "\n📖 Found #{highlights.length} highlights for this book. " \
294
+ "Select highlights to curate:"
295
+
296
+ result = prompt.multi_select("", highlight_choices,
297
+ per_page: 10, echo: false,
298
+ symbols: { marker: "‣", radio_on: "⬢", radio_off: "⬡" })
299
+
300
+ Bridgetown.logger.info "\nUse [↑/↓] to navigate, [SPACE] to select/deselect, " \
301
+ "[↩︎ ENTER] when ✅ done."
302
+ Bridgetown.logger.info "Press [Ctrl+C] to go back to book selection."
303
+ result
304
+ rescue TTY::Reader::InputInterrupt
305
+ # User pressed Ctrl+C, return nil to go back to book selection
306
+ nil
307
+ end
308
+
309
+ def load_existing_selections(service, book_id)
310
+ service.load_existing_selections(book_id)
311
+ end
312
+
313
+ def build_highlight_choices(highlights, selected_ids)
314
+ highlights.map do |h|
315
+ snippet = h["text"][0, 80]
316
+ snippet += "..." if h["text"].length > 80
317
+ {
318
+ name: snippet,
319
+ value: h["id"],
320
+ checked: selected_ids.include?(h["id"]),
321
+ }
322
+ end
323
+ end
324
+
325
+ def save_selected_highlights(service, book_id, highlights, selected_highlight_ids)
326
+ return handle_no_selection if selected_highlight_ids.empty?
327
+
328
+ curated_highlights = filter_selected_highlights(highlights, selected_highlight_ids)
329
+ saved_file = save_highlights_to_file(service, book_id, curated_highlights)
330
+ log_save_success(saved_file)
331
+ end
332
+
333
+ def filter_selected_highlights(highlights, selected_ids)
334
+ highlights.select { |h| selected_ids.include?(h["id"]) }
335
+ end
336
+
337
+ def save_highlights_to_file(service, book_id, highlights)
338
+ service.save_curated_highlights(book_id, highlights)
339
+ end
340
+ end
341
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReadwiseCurator
4
+ module CLIHelpers
5
+ def clear_screen
6
+ system("clear") || system("cls")
7
+ end
8
+
9
+ def show_pagination_header(choices, current_page, total_pages, page_size)
10
+ start_item = (current_page * page_size) + 1
11
+ end_item = [((current_page + 1) * page_size), choices.length].min
12
+
13
+ Bridgetown.logger.info "\n📚 Readwise Library (#{choices.length} items)"
14
+ Bridgetown.logger.info "Icons: 📚 Books • 📄 Articles • 🐦 Tweets • 🎧 Podcasts • 📋 Supplementals"
15
+ Bridgetown.logger.info "* indicates existing page"
16
+ Bridgetown.logger.info "Page #{current_page + 1}/#{total_pages} " \
17
+ "(showing #{start_item}-#{end_item})\n\n"
18
+ end
19
+
20
+ def display_book_count(count)
21
+ Bridgetown.logger.info "\n📚 Found #{count} items in your Readwise library"
22
+ Bridgetown.logger.info "Icons: 📚 Books • 📄 Articles • 🐦 Tweets • 🎧 Podcasts • 📋 Supplementals"
23
+ Bridgetown.logger.info "* indicates existing page • Use ↑/↓ to navigate, ENTER to select\n\n"
24
+ end
25
+
26
+ def get_category_icon(category)
27
+ case category&.downcase
28
+ when "books"
29
+ "📚"
30
+ when "tweets"
31
+ "🐦"
32
+ when "podcasts"
33
+ "🎧"
34
+ when "supplementals"
35
+ "📋"
36
+ else
37
+ "📄"
38
+ end
39
+ end
40
+
41
+ def handle_empty_highlights
42
+ Bridgetown.logger.info "\n📖 Found 0 highlights for this book"
43
+ Bridgetown.logger.info "This book has no highlights to curate."
44
+ end
45
+
46
+ def display_highlight_instructions(highlight_count)
47
+ Bridgetown.logger.info "\n📖 Found #{highlight_count} highlights for this book"
48
+ Bridgetown.logger.info "Use [↑/↓] to navigate, [SPACE] to select/deselect, " \
49
+ "[↩︎ ENTER] when ✅ done."
50
+ Bridgetown.logger.info "Press [Ctrl+C] to go back to book selection.\n\n"
51
+ end
52
+
53
+ def handle_no_selection
54
+ Bridgetown.logger.info "No highlights selected."
55
+ end
56
+
57
+ def log_save_success(saved_file)
58
+ Bridgetown.logger.info "Curated highlights saved to #{saved_file}."
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "fileutils"
7
+
8
+ module ReadwiseCurator
9
+ class HighlightService
10
+ attr_reader :site, :api_key
11
+
12
+ def initialize(site, options = {})
13
+ @site = site
14
+ @api_key = options[:api_key] || site.config.readwise_curator&.api_key || ENV.fetch(
15
+ "READWISE_TOKEN", nil
16
+ )
17
+ raise "Readwise API key not configured" unless @api_key
18
+ end
19
+
20
+ def fetch_highlights_for_book(book_id)
21
+ highlights = []
22
+ next_url = "https://readwise.io/api/v2/highlights/?book_id=#{book_id}"
23
+
24
+ while next_url
25
+ response = make_request(next_url)
26
+ highlights.concat(response["results"])
27
+ next_url = response["next"]
28
+ end
29
+
30
+ highlights
31
+ end
32
+
33
+ def save_curated_highlights(book_id, highlights)
34
+ curated_dir = File.join(site.root_dir, "src/_data/readwise/curated_highlights")
35
+ FileUtils.mkdir_p(curated_dir)
36
+
37
+ curated_file = File.join(curated_dir, "#{book_id}.json")
38
+ minimal = highlights.map { |h| h.slice("id", "text", "note", "highlighted_at") }
39
+
40
+ File.write(curated_file, JSON.pretty_generate(minimal))
41
+ curated_file
42
+ end
43
+
44
+ def load_existing_selections(book_id)
45
+ curated_dir = File.join(site.root_dir, "src/_data/readwise/curated_highlights")
46
+ curated_file = File.join(curated_dir, "#{book_id}.json")
47
+
48
+ return [] unless File.exist?(curated_file)
49
+
50
+ JSON.parse(File.read(curated_file)).map { |h| h["id"] }
51
+ end
52
+
53
+ private
54
+
55
+ def make_request(url)
56
+ uri = URI(url)
57
+ http = Net::HTTP.new(uri.host, uri.port)
58
+ http.use_ssl = true
59
+ request = Net::HTTP::Get.new(uri)
60
+ request["Authorization"] = "Token #{@api_key}"
61
+
62
+ response = http.request(request)
63
+ JSON.parse(response.body)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReadwiseCurator
4
+ VERSION = "0.1.0"
5
+ end
data/logfile ADDED
@@ -0,0 +1,9 @@
1
+ 2025-08-13 09:08:11.135 CEST [6019] LOG: starting PostgreSQL 17.4 on aarch64-apple-darwin24.3.0, compiled by Apple clang version 16.0.0 (clang-1600.0.26.6), 64-bit
2
+ 2025-08-13 09:08:11.226 CEST [6019] LOG: listening on IPv6 address "::1", port 5432
3
+ 2025-08-13 09:08:11.226 CEST [6019] LOG: listening on IPv6 address "fe80::1%lo0", port 5432
4
+ 2025-08-13 09:08:11.226 CEST [6019] LOG: listening on IPv4 address "127.0.0.1", port 5432
5
+ 2025-08-13 09:08:11.227 CEST [6019] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432"
6
+ 2025-08-13 09:08:11.343 CEST [6092] LOG: database system was shut down at 2025-08-10 18:44:21 CEST
7
+ 2025-08-13 09:08:11.428 CEST [6019] LOG: database system is ready to accept connections
8
+ 2025-08-13 09:13:11.272 CEST [6082] LOG: checkpoint starting: time
9
+ 2025-08-13 09:13:11.279 CEST [6082] LOG: checkpoint complete: wrote 3 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.002 s, sync=0.001 s, total=0.010 s; sync files=2, longest=0.001 s, average=0.001 s; distance=0 kB, estimate=0 kB; lsn=0/15527A8, redo lsn=0/1552750
data/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "readwise_curator",
3
+ "version": "0.1.0",
4
+ "main": "frontend/javascript/index.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/username/readwise_curator.git"
8
+ },
9
+ "author": "Bridgetown Maintainers <maintainers@bridgetownrb.com>",
10
+ "homepage": "https://www.bridgetownrb.com",
11
+ "license": "MIT",
12
+ "private": false,
13
+ "files": [
14
+ "frontend"
15
+ ]
16
+ }
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bridgetown_readwise_curator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pablo Jimeno
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-08-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bridgetown
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.0.beta5
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 2.0.0.beta5
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: tty-prompt
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.23'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.23'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop-bridgetown
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.3'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.3'
89
+ description: A Bridgetown plugin that provides an interactive CLI to curate and manage
90
+ Readwise highlights directly within your Bridgetown site.
91
+ email: hello@pablojimeno.com
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - ".github/workflows/tests.yml"
97
+ - ".gitignore"
98
+ - ".rubocop.yml"
99
+ - CHANGELOG.md
100
+ - Gemfile
101
+ - LICENSE.txt
102
+ - README.md
103
+ - Rakefile
104
+ - bridgetown.automation.rb
105
+ - bridgetown_readwise_curator.gemspec
106
+ - components/readwise_curator/layout_help.liquid
107
+ - components/readwise_curator/plugin_component.rb
108
+ - content/readwise_curator/example_page.md
109
+ - content/readwise_curator/train-on-rails.jpeg
110
+ - layouts/readwise_curator/layout.html
111
+ - lib/bridgetown_readwise_curator.rb
112
+ - lib/readwise_curator/builder.rb
113
+ - lib/readwise_curator/cli.rb
114
+ - lib/readwise_curator/cli_helpers.rb
115
+ - lib/readwise_curator/highlight_service.rb
116
+ - lib/readwise_curator/version.rb
117
+ - logfile
118
+ - package.json
119
+ homepage: https://github.com/pablojimeno/bridgetown_readwise_curator
120
+ licenses:
121
+ - MIT
122
+ metadata:
123
+ bridgetown_plugin: 'true'
124
+ source_code_uri: https://github.com/pablojimeno/bridgetown_readwise_curator
125
+ bug_tracker_uri: https://github.com/pablojimeno/bridgetown_readwise_curator/issues
126
+ changelog_uri: https://github.com/pablojimeno/bridgetown_readwise_curator/releases
127
+ homepage_uri: https://github.com/pablojimeno/bridgetown_readwise_curator
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 2.7.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.4.19
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Curate Readwise highlights for Bridgetown sites
147
+ test_files: []