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 +7 -0
- data/.github/workflows/tests.yml +32 -0
- data/.gitignore +40 -0
- data/.rubocop.yml +23 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +114 -0
- data/Rakefile +11 -0
- data/bridgetown.automation.rb +72 -0
- data/bridgetown_readwise_curator.gemspec +34 -0
- data/components/readwise_curator/layout_help.liquid +1 -0
- data/components/readwise_curator/plugin_component.rb +17 -0
- data/content/readwise_curator/example_page.md +8 -0
- data/content/readwise_curator/train-on-rails.jpeg +0 -0
- data/layouts/readwise_curator/layout.html +9 -0
- data/lib/bridgetown_readwise_curator.rb +24 -0
- data/lib/readwise_curator/builder.rb +46 -0
- data/lib/readwise_curator/cli.rb +341 -0
- data/lib/readwise_curator/cli_helpers.rb +61 -0
- data/lib/readwise_curator/highlight_service.rb +66 -0
- data/lib/readwise_curator/version.rb +5 -0
- data/logfile +9 -0
- data/package.json +16 -0
- metadata +147 -0
    
        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
    
    
    
        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,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 | 
            +
            
         | 
| Binary file | 
| @@ -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
         | 
    
        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: []
         |