confinement 0.0.1
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/.gitignore +8 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/rake +29 -0
- data/bin/setup +8 -0
- data/confinement.gemspec +32 -0
- data/exe/confinement +237 -0
- data/lib/confinement.rb +570 -0
- data/lib/confinement/version.rb +3 -0
- metadata +129 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: e85abea697c0437610597a3c221c833d97931bace819ba66ebb7c02d48ed6c20
         | 
| 4 | 
            +
              data.tar.gz: b611ce215a7c6057fe2e874707e314b0644f7f0732229810127cd7c7702fb572
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: bdc4a25ecacc5d1c378f589d28cef55e63cfd3bbd6353ba9b1cf2439ae08f7bc219f2f77f2257d4bcf887fb7e85c8a6d659097f4a7052c5e47b72d7727b9cbde
         | 
| 7 | 
            +
              data.tar.gz: 17527d0a0f6307a5c2dc2b7b2ae445294c03afe5ffea69f20f3dabd3a6ac0a432181d82a8be7985666e01c875ff03f128aacc7add2a6968bda4770b17512c8a4
         | 
    
        data/.gitignore
    ADDED
    
    
    
        data/.ruby-version
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            2.7.0
         | 
    
        data/.travis.yml
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/Gemfile.lock
    ADDED
    
    | @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            PATH
         | 
| 2 | 
            +
              remote: .
         | 
| 3 | 
            +
              specs:
         | 
| 4 | 
            +
                confinement (0.0.1)
         | 
| 5 | 
            +
                  erubi
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            GEM
         | 
| 8 | 
            +
              remote: https://rubygems.org/
         | 
| 9 | 
            +
              specs:
         | 
| 10 | 
            +
                byebug (11.1.1)
         | 
| 11 | 
            +
                coderay (1.1.2)
         | 
| 12 | 
            +
                erubi (1.9.0)
         | 
| 13 | 
            +
                method_source (1.0.0)
         | 
| 14 | 
            +
                minitest (5.14.0)
         | 
| 15 | 
            +
                pry (0.13.0)
         | 
| 16 | 
            +
                  coderay (~> 1.1)
         | 
| 17 | 
            +
                  method_source (~> 1.0)
         | 
| 18 | 
            +
                pry-byebug (3.9.0)
         | 
| 19 | 
            +
                  byebug (~> 11.0)
         | 
| 20 | 
            +
                  pry (~> 0.13.0)
         | 
| 21 | 
            +
                rake (13.0.1)
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            PLATFORMS
         | 
| 24 | 
            +
              ruby
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            DEPENDENCIES
         | 
| 27 | 
            +
              bundler (~> 2.0)
         | 
| 28 | 
            +
              confinement!
         | 
| 29 | 
            +
              minitest (~> 5.0)
         | 
| 30 | 
            +
              pry-byebug
         | 
| 31 | 
            +
              rake (~> 13.0)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            BUNDLED WITH
         | 
| 34 | 
            +
               2.1.4
         | 
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            The MIT License (MIT)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2020 Zach Ahn
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in
         | 
| 13 | 
            +
            all copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 21 | 
            +
            THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            # Confinement
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            A static site generator for when you're stuck at home.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Assets are generated through the (nightly) [Parcel bundler][parcel2].
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
            ## Installation
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            Confinement requires Ruby 2.7+, Node 10+, and Yarn.
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            Add this line to your application's Gemfile:
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            ```ruby
         | 
| 15 | 
            +
            gem "confinement"
         | 
| 16 | 
            +
            ```
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            And then execute:
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                $ bundle
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            Or install it yourself as:
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                $ gem install confinement
         | 
| 25 | 
            +
             | 
| 26 | 
            +
             | 
| 27 | 
            +
            ## Usage
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            ```sh
         | 
| 30 | 
            +
            confinement init path/to/new/site
         | 
| 31 | 
            +
            ```
         | 
| 32 | 
            +
             | 
| 33 | 
            +
             | 
| 34 | 
            +
            ## Development
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            After checking out the repo, run `bin/setup` to install dependencies. Then, run
         | 
| 37 | 
            +
            `rake test` to run the tests. You can also run `bin/console` for an interactive
         | 
| 38 | 
            +
            prompt that will allow you to experiment.
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            To install this gem onto your local machine, run `bundle exec rake install`. To
         | 
| 41 | 
            +
            release a new version, update the version number in `version.rb`, and then run
         | 
| 42 | 
            +
            `bundle exec rake release`, which will create a git tag for the version, push
         | 
| 43 | 
            +
            git commits and tags, and push the `.gem` file to
         | 
| 44 | 
            +
            [rubygems.org](https://rubygems.org).
         | 
| 45 | 
            +
             | 
| 46 | 
            +
             | 
| 47 | 
            +
            ## Contributing
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            Bug reports and pull requests are welcome on GitHub at
         | 
| 50 | 
            +
            https://github.com/zachahn/confinement.
         | 
| 51 | 
            +
             | 
| 52 | 
            +
             | 
| 53 | 
            +
            ## License
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            The gem is available as open source under the terms of the
         | 
| 56 | 
            +
            [MIT License](https://opensource.org/licenses/MIT).
         | 
| 57 | 
            +
             | 
| 58 | 
            +
             | 
| 59 | 
            +
            [parcel2]: https://github.com/parcel-bundler/parcel/tree/v2
         | 
    
        data/Rakefile
    ADDED
    
    
    
        data/bin/console
    ADDED
    
    | @@ -0,0 +1,14 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "bundler/setup"
         | 
| 4 | 
            +
            require "confinement"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # You can add fixtures and/or initialization code here to make experimenting
         | 
| 7 | 
            +
            # with your gem easier. You can also use a different console, if you like.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # (If you use this, don't forget to add pry to your Gemfile!)
         | 
| 10 | 
            +
            # require "pry"
         | 
| 11 | 
            +
            # Pry.start
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            require "irb"
         | 
| 14 | 
            +
            IRB.start(__FILE__)
         | 
    
        data/bin/rake
    ADDED
    
    | @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # This file was generated by Bundler.
         | 
| 6 | 
            +
            #
         | 
| 7 | 
            +
            # The application 'rake' is installed as part of a gem, and
         | 
| 8 | 
            +
            # this file is here to facilitate running it.
         | 
| 9 | 
            +
            #
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            require "pathname"
         | 
| 12 | 
            +
            ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
         | 
| 13 | 
            +
              Pathname.new(__FILE__).realpath)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            bundle_binstub = File.expand_path("../bundle", __FILE__)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            if File.file?(bundle_binstub)
         | 
| 18 | 
            +
              if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
         | 
| 19 | 
            +
                load(bundle_binstub)
         | 
| 20 | 
            +
              else
         | 
| 21 | 
            +
                abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
         | 
| 22 | 
            +
            Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            require "rubygems"
         | 
| 27 | 
            +
            require "bundler/setup"
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            load Gem.bin_path("rake", "rake")
         | 
    
        data/bin/setup
    ADDED
    
    
    
        data/confinement.gemspec
    ADDED
    
    | @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            lib = File.expand_path("../lib", __FILE__)
         | 
| 2 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 3 | 
            +
            require "confinement/version"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Gem::Specification.new do |spec|
         | 
| 6 | 
            +
              spec.name = "confinement"
         | 
| 7 | 
            +
              spec.version = Confinement::VERSION
         | 
| 8 | 
            +
              spec.authors = ["Zach Ahn"]
         | 
| 9 | 
            +
              spec.email = ["engineering@zachahn.com"]
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              spec.summary = "Static site generator for when you're stuck at home"
         | 
| 12 | 
            +
              spec.homepage = "https://github.com/zachahn/confinement"
         | 
| 13 | 
            +
              spec.license = "MIT"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              # Specify which files should be added to the gem when it is released.
         | 
| 16 | 
            +
              # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
         | 
| 17 | 
            +
              spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
         | 
| 18 | 
            +
                `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
              spec.bindir = "exe"
         | 
| 21 | 
            +
              spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 22 | 
            +
              spec.require_paths = ["lib"]
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              spec.required_ruby_version = ">= 2.7.0"
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              spec.add_development_dependency "bundler", "~> 2.0"
         | 
| 27 | 
            +
              spec.add_development_dependency "rake", "~> 13.0"
         | 
| 28 | 
            +
              spec.add_development_dependency "minitest", "~> 5.0"
         | 
| 29 | 
            +
              spec.add_development_dependency "pry-byebug"
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              spec.add_runtime_dependency("erubi")
         | 
| 32 | 
            +
            end
         | 
    
        data/exe/confinement
    ADDED
    
    | @@ -0,0 +1,237 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            if File.exist?(File.expand_path("../.git", __dir__))
         | 
| 5 | 
            +
              lib = File.expand_path("../lib", __dir__)
         | 
| 6 | 
            +
              $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 7 | 
            +
            end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require "optparse"
         | 
| 10 | 
            +
            require "pathname"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            require "confinement/version"
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            module Confinement
         | 
| 15 | 
            +
              class CLI
         | 
| 16 | 
            +
                def self.subcommands
         | 
| 17 | 
            +
                  @subcommands ||= {
         | 
| 18 | 
            +
                    "init" => Init.new,
         | 
| 19 | 
            +
                  }
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def self.script_name
         | 
| 23 | 
            +
                  @script_name ||= File.basename(__FILE__)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def optparser
         | 
| 27 | 
            +
                  @optparser ||= OptionParser.new do |opts|
         | 
| 28 | 
            +
                    opts.banner = "Usage: #{CLI.script_name} [options] [subcommand [options]]"
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    opts.on("--version", "Print version (#{Confinement::VERSION})") do
         | 
| 31 | 
            +
                      puts Confinement::VERSION
         | 
| 32 | 
            +
                      exit
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    opts.on("-h", "--help", "Prints this help message") do
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def call(argv)
         | 
| 41 | 
            +
                  optparser.order!(argv)
         | 
| 42 | 
            +
                  subcommand_name = argv.shift
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  if subcommand_name.nil?
         | 
| 45 | 
            +
                    puts(optparser)
         | 
| 46 | 
            +
                    exit
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  if !self.class.subcommands.key?(subcommand_name)
         | 
| 50 | 
            +
                    $stderr.puts("#{CLI.script_name}: `#{subcommand_name}` is not valid command")
         | 
| 51 | 
            +
                    $stderr.puts
         | 
| 52 | 
            +
                    $stderr.puts(optparser)
         | 
| 53 | 
            +
                    exit(1)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  subcommand = self.class.subcommands.fetch(subcommand_name)
         | 
| 57 | 
            +
                  subcommand.optparser.parse!(argv)
         | 
| 58 | 
            +
                  subcommand.call(argv)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                private
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                class Init
         | 
| 64 | 
            +
                  def optparser
         | 
| 65 | 
            +
                    @options ||= {}
         | 
| 66 | 
            +
                    @optparser ||= OptionParser.new do |opts|
         | 
| 67 | 
            +
                      opts.banner = "Usage: #{CLI.script_name} init [directory]"
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                      opts.on("--force", "Write files without prompting") do
         | 
| 70 | 
            +
                        @force = true
         | 
| 71 | 
            +
                      end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                      opts.on("--skip", "Skip files without prompting") do
         | 
| 74 | 
            +
                        @skip = true
         | 
| 75 | 
            +
                      end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                      opts.on("-h", "--help", "Prints this help message") do
         | 
| 78 | 
            +
                        puts opts
         | 
| 79 | 
            +
                        exit
         | 
| 80 | 
            +
                      end
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def call(argv)
         | 
| 85 | 
            +
                    if argv.size != 1
         | 
| 86 | 
            +
                      puts optparser
         | 
| 87 | 
            +
                      exit
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    require "open3"
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    root = Pathname.new(argv.first).expand_path
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    templates = DATA.read.split(/^==> /)[1..-1]
         | 
| 95 | 
            +
                    templates = templates.filter_map do |template|
         | 
| 96 | 
            +
                      path, *body = template.lines
         | 
| 97 | 
            +
                      body = body.join("")
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                      [Pathname.new(File.join(root, path.strip)), body.strip + "\n"]
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    templates
         | 
| 103 | 
            +
                      .map { |path, _| path.dirname }
         | 
| 104 | 
            +
                      .uniq
         | 
| 105 | 
            +
                      .each { |path| mkdir(path, root) }
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    templates.each do |path, body|
         | 
| 108 | 
            +
                      write(path, body, root)
         | 
| 109 | 
            +
                    end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                    Dir.chdir(root) do
         | 
| 112 | 
            +
                      run(["yarn", "init", "--yes"])
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                      run(["yarn", "add", "--dev", "parcel@nightly"])
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  private
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  def run(command)
         | 
| 121 | 
            +
                    status("exec", command.join(" "))
         | 
| 122 | 
            +
                    Open3.capture2(*command)
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  def write(path, body, root)
         | 
| 126 | 
            +
                    relpath = path.relative_path_from(root.dirname)
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    if path.exist?
         | 
| 129 | 
            +
                      if @skip
         | 
| 130 | 
            +
                        status("skip", relpath)
         | 
| 131 | 
            +
                      elsif @force
         | 
| 132 | 
            +
                        status("force", relpath)
         | 
| 133 | 
            +
                        path.write(body)
         | 
| 134 | 
            +
                      elsif yes?("overwrite #{path}?")
         | 
| 135 | 
            +
                        status("overwrite", relpath)
         | 
| 136 | 
            +
                        path.write(body)
         | 
| 137 | 
            +
                      else
         | 
| 138 | 
            +
                        status("skip", relpath)
         | 
| 139 | 
            +
                      end
         | 
| 140 | 
            +
                    else
         | 
| 141 | 
            +
                      status("write", relpath)
         | 
| 142 | 
            +
                      path.write(body)
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  def mkdir(dir, root)
         | 
| 147 | 
            +
                    if dir.exist?
         | 
| 148 | 
            +
                      status("exist", dir.relative_path_from(root.dirname))
         | 
| 149 | 
            +
                    else
         | 
| 150 | 
            +
                      status("mkdir", dir.relative_path_from(root.dirname))
         | 
| 151 | 
            +
                      dir.mkdir
         | 
| 152 | 
            +
                    end
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                  def status(action, object)
         | 
| 156 | 
            +
                    space = 12 - action.size
         | 
| 157 | 
            +
                    puts "    #{action}#{" " * space}#{object}"
         | 
| 158 | 
            +
                  end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                  def yes?(question)
         | 
| 161 | 
            +
                    print "#{question} (y/N) "
         | 
| 162 | 
            +
                    answer = $stdin.gets
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                    return false if answer.nil?
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                    /^y/i.match?(answer)
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
              end
         | 
| 170 | 
            +
            end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
            Confinement::CLI.new.call(ARGV.dup)
         | 
| 173 | 
            +
             | 
| 174 | 
            +
            __END__
         | 
| 175 | 
            +
            ==> boot.rb
         | 
| 176 | 
            +
            require "confinement"
         | 
| 177 | 
            +
             | 
| 178 | 
            +
            Confinement.root = __dir__
         | 
| 179 | 
            +
             | 
| 180 | 
            +
            Confinement.site = Confinement::Builder.new(
         | 
| 181 | 
            +
              root: Confinement.root,
         | 
| 182 | 
            +
              assets: "assets",
         | 
| 183 | 
            +
              contents: "contents",
         | 
| 184 | 
            +
              layouts: "layouts",
         | 
| 185 | 
            +
              config: {
         | 
| 186 | 
            +
                index: "index.html",
         | 
| 187 | 
            +
              }
         | 
| 188 | 
            +
            )
         | 
| 189 | 
            +
             | 
| 190 | 
            +
            ==> build.rb
         | 
| 191 | 
            +
            require_relative "boot"
         | 
| 192 | 
            +
             | 
| 193 | 
            +
            using Confinement::Easier
         | 
| 194 | 
            +
             | 
| 195 | 
            +
            Confinement.site.contents do |contents, layouts, dest|
         | 
| 196 | 
            +
              dest["/"] = Confinement::Content.new(
         | 
| 197 | 
            +
                layout: layouts.join("default.html.erb"),
         | 
| 198 | 
            +
                input_path: contents.join("index.html.erb"),
         | 
| 199 | 
            +
                renderers: [Confinement::Renderer::Erb.new]
         | 
| 200 | 
            +
              )
         | 
| 201 | 
            +
            end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
            Confinement.site.assets do |assets, dest|
         | 
| 204 | 
            +
              dest["/assets/application.js"] = Confinement::Asset.new(
         | 
| 205 | 
            +
                input_path: assets.join("application.js"),
         | 
| 206 | 
            +
                entrypoint: true,
         | 
| 207 | 
            +
              )
         | 
| 208 | 
            +
             | 
| 209 | 
            +
              dest["/assets/application.css"] = Confinement::Asset.new(
         | 
| 210 | 
            +
                input_path: assets.join("application.css"),
         | 
| 211 | 
            +
                entrypoint: false,
         | 
| 212 | 
            +
              )
         | 
| 213 | 
            +
            end
         | 
| 214 | 
            +
             | 
| 215 | 
            +
            Confinement.site.layouts do |layouts, dest|
         | 
| 216 | 
            +
              dest[layouts.join("default.html.erb")] = Confinement::Layout.new(
         | 
| 217 | 
            +
                renderers: [Confinement::Renderer::Erb.new]
         | 
| 218 | 
            +
              )
         | 
| 219 | 
            +
            end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
            ==> write.rb
         | 
| 222 | 
            +
            require_relative "build"
         | 
| 223 | 
            +
             | 
| 224 | 
            +
            Confinement::Publish
         | 
| 225 | 
            +
              .new(Confinement.site)
         | 
| 226 | 
            +
              .write(Confinement.root.join("public"))
         | 
| 227 | 
            +
             | 
| 228 | 
            +
            ==> assets/application.js
         | 
| 229 | 
            +
            import "./application.css";
         | 
| 230 | 
            +
             | 
| 231 | 
            +
            ==> assets/application.css
         | 
| 232 | 
            +
             | 
| 233 | 
            +
            ==> layouts/default.html.erb
         | 
| 234 | 
            +
             | 
| 235 | 
            +
            ==> contents/index.html.erb
         | 
| 236 | 
            +
             | 
| 237 | 
            +
             | 
    
        data/lib/confinement.rb
    ADDED
    
    | @@ -0,0 +1,570 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            if Gem.loaded_specs.has_key?("pry-byebug")
         | 
| 4 | 
            +
              require "pry-byebug"
         | 
| 5 | 
            +
            elsif Gem.loaded_specs.has_key?("pry-byebug")
         | 
| 6 | 
            +
              require "pry"
         | 
| 7 | 
            +
            end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # standard library
         | 
| 10 | 
            +
            require "digest"
         | 
| 11 | 
            +
            require "open3"
         | 
| 12 | 
            +
            require "pathname"
         | 
| 13 | 
            +
            require "yaml"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            # gems
         | 
| 16 | 
            +
            require "erubi"
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            # internal
         | 
| 19 | 
            +
            require_relative "confinement/version"
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            module Confinement
         | 
| 22 | 
            +
              class Error < StandardError
         | 
| 23 | 
            +
                class PathDoesNotExist < Error; end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              FRONTMATTER_REGEX =
         | 
| 27 | 
            +
                /\A
         | 
| 28 | 
            +
                  (?<possible_frontmatter_section>
         | 
| 29 | 
            +
                    (?<frontmatter>^---\n
         | 
| 30 | 
            +
                    .*?)
         | 
| 31 | 
            +
                    ^---\n)?
         | 
| 32 | 
            +
                  (?<body>.*)
         | 
| 33 | 
            +
                \z/mx
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              PATH_INCLUDE_PATH_REGEX = /\A\.\.(\z|\/)/
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              module Easier
         | 
| 38 | 
            +
                # NOTE: Avoid state - Pathname is supposed to be immutable
         | 
| 39 | 
            +
                refine Pathname do
         | 
| 40 | 
            +
                  # Pathname#join and File.join behave very differently
         | 
| 41 | 
            +
                  #
         | 
| 42 | 
            +
                  # - Pathname.new("foo").join("/bar") # => Pathname.new("/bar")
         | 
| 43 | 
            +
                  # - File.join("foo", "/bar") # => "foo/bar"
         | 
| 44 | 
            +
                  def concat(*parts)
         | 
| 45 | 
            +
                    Pathname.new(File.join(self, *parts))
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  def include?(other)
         | 
| 49 | 
            +
                    difference = other.relative_path_from(self)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    !PATH_INCLUDE_PATH_REGEX.match?(difference.to_s)
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                refine String do
         | 
| 56 | 
            +
                  def frontmatter_and_body(strip: true)
         | 
| 57 | 
            +
                    matches = FRONTMATTER_REGEX.match(self)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    return [{}, self] if matches["frontmatter"].nil?
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    frontmatter = YAML.safe_load(matches["frontmatter"])
         | 
| 62 | 
            +
                    body = matches["body"] || ""
         | 
| 63 | 
            +
                    body = body.strip if strip
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    [frontmatter, body]
         | 
| 66 | 
            +
                  rescue ArgumentError
         | 
| 67 | 
            +
                    [{}, self]
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              using Easier
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              class << self
         | 
| 75 | 
            +
                attr_reader :root
         | 
| 76 | 
            +
                attr_accessor :site
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def root=(path)
         | 
| 79 | 
            +
                  # NOTE: Pathname.new(Pathname.new(".")) == Pathname.new(".")
         | 
| 80 | 
            +
                  path = Pathname.new(path).expand_path
         | 
| 81 | 
            +
                  path = path.expand_path
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  if !path.exist?
         | 
| 84 | 
            +
                    raise Error::PathDoesNotExist, "Root path does not exist: #{path}"
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  @root = path
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
              end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
              class Builder
         | 
| 92 | 
            +
                def initialize(root:, assets:, contents:, layouts:, config: {})
         | 
| 93 | 
            +
                  @root = root
         | 
| 94 | 
            +
                  @assets = assets
         | 
| 95 | 
            +
                  @contents = contents
         | 
| 96 | 
            +
                  @layouts = layouts
         | 
| 97 | 
            +
                  @config = {
         | 
| 98 | 
            +
                    index: config.fetch(:index, "index.html")
         | 
| 99 | 
            +
                  }
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  @output_root = root.concat(config.fetch(:destination_root, "public"))
         | 
| 102 | 
            +
                  @assets_root = @output_root.concat(config.fetch(:assets_subdirectory, "assets"))
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  @representation = Representation.new
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                attr_reader :root
         | 
| 108 | 
            +
                attr_reader :output_root
         | 
| 109 | 
            +
                attr_reader :assets_root
         | 
| 110 | 
            +
                attr_reader :representation
         | 
| 111 | 
            +
                attr_reader :config
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                def contents_path
         | 
| 114 | 
            +
                  @contents_path ||= @root.concat(@contents)
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def layouts_path
         | 
| 118 | 
            +
                  @layouts_path ||= @root.concat(@layouts)
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def assets_path
         | 
| 122 | 
            +
                  @assets_path ||= @root.concat(@assets)
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                def layouts
         | 
| 126 | 
            +
                  if !layouts_path.exist?
         | 
| 127 | 
            +
                    raise PathDoesNotExist, "Layouts path doesn't exist: #{layouts_path}"
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  yield(layouts_path, @representation.layouts)
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def assets
         | 
| 134 | 
            +
                  if !assets_path.exist?
         | 
| 135 | 
            +
                    raise PathDoesNotExist, "Assets path doesn't exist: #{assets_path}"
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  yield(assets_path, @representation.assets)
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def contents
         | 
| 142 | 
            +
                  if !contents_path.exist?
         | 
| 143 | 
            +
                    raise PathDoesNotExist, "Contents path doesn't exist: #{contents_path}"
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  yield(contents_path, layouts_path, @representation.contents)
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
              end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
              class Representation
         | 
| 151 | 
            +
                include Enumerable
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                def initialize
         | 
| 154 | 
            +
                  @lookup = {}
         | 
| 155 | 
            +
                  @layouts_lookup = {}
         | 
| 156 | 
            +
                  @only_assets = []
         | 
| 157 | 
            +
                  @only_contents = []
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                attr_reader :only_assets
         | 
| 161 | 
            +
                attr_reader :layouts_lookup
         | 
| 162 | 
            +
                attr_reader :only_contents
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def fetch(key)
         | 
| 165 | 
            +
                  if !@lookup.key?(key)
         | 
| 166 | 
            +
                    raise "Not represented!"
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  @lookup[key]
         | 
| 170 | 
            +
                end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                def each
         | 
| 173 | 
            +
                  if !block_given?
         | 
| 174 | 
            +
                    return enum_for(:each)
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  @lookup.each do |identifier, page|
         | 
| 178 | 
            +
                    yield(identifier, page)
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                def layouts
         | 
| 183 | 
            +
                  Setter.new(@layouts_lookup) do |path, layout|
         | 
| 184 | 
            +
                    layout.input_path = path
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                def assets
         | 
| 189 | 
            +
                  Setter.new(@lookup) do |identifier, asset|
         | 
| 190 | 
            +
                    @only_assets.push(asset)
         | 
| 191 | 
            +
                    asset.url_path = identifier
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                def contents
         | 
| 196 | 
            +
                  Setter.new(@lookup) do |identifier, content|
         | 
| 197 | 
            +
                    @only_contents.push(content)
         | 
| 198 | 
            +
                    content.url_path = identifier
         | 
| 199 | 
            +
                  end
         | 
| 200 | 
            +
                end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                def routes_getter
         | 
| 203 | 
            +
                  LookupGetter.new(@lookup)
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                def layouts_getter
         | 
| 207 | 
            +
                  LookupGetter.new(@layouts_lookup)
         | 
| 208 | 
            +
                end
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                class Setter
         | 
| 211 | 
            +
                  def initialize(lookup, &block)
         | 
| 212 | 
            +
                    @lookup = lookup
         | 
| 213 | 
            +
                    @block = block
         | 
| 214 | 
            +
                  end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                  def []=(key, value)
         | 
| 217 | 
            +
                    @block&.call(key, value)
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                    @lookup[key] = value
         | 
| 220 | 
            +
                  end
         | 
| 221 | 
            +
                end
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                class LookupGetter
         | 
| 224 | 
            +
                  def initialize(lookup)
         | 
| 225 | 
            +
                    @lookup = lookup
         | 
| 226 | 
            +
                  end
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                  def [](key)
         | 
| 229 | 
            +
                    @lookup.fetch(key)
         | 
| 230 | 
            +
                  end
         | 
| 231 | 
            +
                end
         | 
| 232 | 
            +
              end
         | 
| 233 | 
            +
             | 
| 234 | 
            +
              module Blob
         | 
| 235 | 
            +
                attr_accessor :input_path
         | 
| 236 | 
            +
                attr_accessor :output_path
         | 
| 237 | 
            +
                attr_reader :url_path
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                def url_path=(path)
         | 
| 240 | 
            +
                  if path.nil?
         | 
| 241 | 
            +
                    @url_path = nil
         | 
| 242 | 
            +
                    return
         | 
| 243 | 
            +
                  end
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                  path = path.to_s
         | 
| 246 | 
            +
                  if path[0] != "/"
         | 
| 247 | 
            +
                    path = "/#{path}"
         | 
| 248 | 
            +
                  end
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                  @url_path = path
         | 
| 251 | 
            +
                end
         | 
| 252 | 
            +
              end
         | 
| 253 | 
            +
             | 
| 254 | 
            +
              class Asset
         | 
| 255 | 
            +
                include Blob
         | 
| 256 | 
            +
             | 
| 257 | 
            +
                def initialize(input_path:, entrypoint:)
         | 
| 258 | 
            +
                  self.input_path = input_path
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                  @entrypoint = entrypoint
         | 
| 261 | 
            +
                  @url_path = nil
         | 
| 262 | 
            +
                end
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                attr_accessor :rendered_body
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                def entrypoint?
         | 
| 267 | 
            +
                  !!@entrypoint
         | 
| 268 | 
            +
                end
         | 
| 269 | 
            +
              end
         | 
| 270 | 
            +
             | 
| 271 | 
            +
              class Content
         | 
| 272 | 
            +
                include Blob
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                def initialize(layout: nil, input_path: nil, locals: {}, renderers: [])
         | 
| 275 | 
            +
                  self.input_path = input_path
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                  @layout = layout
         | 
| 278 | 
            +
                  @locals = locals
         | 
| 279 | 
            +
                  @renderers = renderers
         | 
| 280 | 
            +
                end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                attr_reader :locals
         | 
| 283 | 
            +
                attr_reader :renderers
         | 
| 284 | 
            +
                attr_reader :layout
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                attr_accessor :rendered_body
         | 
| 287 | 
            +
              end
         | 
| 288 | 
            +
             | 
| 289 | 
            +
              class Layout
         | 
| 290 | 
            +
                def initialize(renderers:)
         | 
| 291 | 
            +
                  @renderers = renderers
         | 
| 292 | 
            +
                end
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                attr_reader :renderers
         | 
| 295 | 
            +
                attr_accessor :input_path
         | 
| 296 | 
            +
              end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
              class Renderer
         | 
| 299 | 
            +
                class Erb
         | 
| 300 | 
            +
                  def call(source, view_context, &block)
         | 
| 301 | 
            +
                    method_name = "_#{Digest::MD5.hexdigest(source)}"
         | 
| 302 | 
            +
             | 
| 303 | 
            +
                    compile(method_name, source, view_context)
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                    view_context.public_send(method_name, &block)
         | 
| 306 | 
            +
                  end
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                  private
         | 
| 309 | 
            +
             | 
| 310 | 
            +
                  def compile(method_name, source, view_context)
         | 
| 311 | 
            +
                    if !view_context.respond_to?(method_name)
         | 
| 312 | 
            +
                      compiled_erb = Erubi::Engine.new(source).src
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                      view_context.instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
         | 
| 315 | 
            +
                        def #{method_name}
         | 
| 316 | 
            +
                          #{compiled_erb}
         | 
| 317 | 
            +
                        end
         | 
| 318 | 
            +
                      RUBY
         | 
| 319 | 
            +
                    end
         | 
| 320 | 
            +
                  end
         | 
| 321 | 
            +
                end
         | 
| 322 | 
            +
              end
         | 
| 323 | 
            +
             | 
| 324 | 
            +
              class Rendering
         | 
| 325 | 
            +
                class ViewContext
         | 
| 326 | 
            +
                  def initialize(routes:, layouts:, locals:, frontmatter:, contents_path:, layouts_path:)
         | 
| 327 | 
            +
                    @routes = routes
         | 
| 328 | 
            +
                    @layouts = layouts
         | 
| 329 | 
            +
             | 
| 330 | 
            +
                    @locals = locals
         | 
| 331 | 
            +
                    @frontmatter = frontmatter
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                    @contents_path = contents_path
         | 
| 334 | 
            +
                    @layouts_path = layouts_path
         | 
| 335 | 
            +
                  end
         | 
| 336 | 
            +
             | 
| 337 | 
            +
                  attr_reader :routes
         | 
| 338 | 
            +
                  attr_reader :layouts
         | 
| 339 | 
            +
                  attr_reader :locals
         | 
| 340 | 
            +
                  attr_reader :frontmatter
         | 
| 341 | 
            +
                  attr_reader :contents_path
         | 
| 342 | 
            +
                  attr_reader :layouts_path
         | 
| 343 | 
            +
             | 
| 344 | 
            +
                  def render(path = nil, inline: nil, layout: nil, renderers:, &block)
         | 
| 345 | 
            +
                    body =
         | 
| 346 | 
            +
                      if inline
         | 
| 347 | 
            +
                        inline
         | 
| 348 | 
            +
                      elsif path
         | 
| 349 | 
            +
                        path.read
         | 
| 350 | 
            +
                      else
         | 
| 351 | 
            +
                        raise %(Must pass in either a Pathname or `inline: 'text'`)
         | 
| 352 | 
            +
                      end
         | 
| 353 | 
            +
             | 
| 354 | 
            +
                    render_chain = RenderChain.new(
         | 
| 355 | 
            +
                      body: body,
         | 
| 356 | 
            +
                      layout: layout,
         | 
| 357 | 
            +
                      renderers: renderers,
         | 
| 358 | 
            +
                      view_context: self
         | 
| 359 | 
            +
                    )
         | 
| 360 | 
            +
                    rendered_body = render_chain.call(&block)
         | 
| 361 | 
            +
             | 
| 362 | 
            +
                    if layout
         | 
| 363 | 
            +
                      layout =
         | 
| 364 | 
            +
                        if layout.is_a?(Layout)
         | 
| 365 | 
            +
                          layout
         | 
| 366 | 
            +
                        elsif layout.is_a?(Pathname)
         | 
| 367 | 
            +
                          layouts[layout]
         | 
| 368 | 
            +
                        else
         | 
| 369 | 
            +
                          raise "Expected layout to be a Layout or Pathname"
         | 
| 370 | 
            +
                        end
         | 
| 371 | 
            +
             | 
| 372 | 
            +
                      layout_render_chain = RenderChain.new(
         | 
| 373 | 
            +
                        body: layout.input_path.read,
         | 
| 374 | 
            +
                        layout: nil,
         | 
| 375 | 
            +
                        renderers: layout.renderers,
         | 
| 376 | 
            +
                        view_context: self
         | 
| 377 | 
            +
                      )
         | 
| 378 | 
            +
             | 
| 379 | 
            +
                      layout_render_chain.call do
         | 
| 380 | 
            +
                        rendered_body
         | 
| 381 | 
            +
                      end
         | 
| 382 | 
            +
                    else
         | 
| 383 | 
            +
                      rendered_body
         | 
| 384 | 
            +
                    end
         | 
| 385 | 
            +
                  end
         | 
| 386 | 
            +
                end
         | 
| 387 | 
            +
             | 
| 388 | 
            +
                class RenderChain
         | 
| 389 | 
            +
                  def initialize(body:, layout:, renderers:, view_context:)
         | 
| 390 | 
            +
                    @body = body
         | 
| 391 | 
            +
                    @layout = layout
         | 
| 392 | 
            +
                    @renderers = renderers
         | 
| 393 | 
            +
                    @view_context = view_context
         | 
| 394 | 
            +
                  end
         | 
| 395 | 
            +
             | 
| 396 | 
            +
                  def call(&block)
         | 
| 397 | 
            +
                    @renderers.reduce(@body) do |memo, renderer|
         | 
| 398 | 
            +
                      renderer.call(memo, @view_context, &block)
         | 
| 399 | 
            +
                    end
         | 
| 400 | 
            +
                  end
         | 
| 401 | 
            +
                end
         | 
| 402 | 
            +
              end
         | 
| 403 | 
            +
             | 
| 404 | 
            +
              class HesitantCompiler
         | 
| 405 | 
            +
                def initialize(site)
         | 
| 406 | 
            +
                  @site = site
         | 
| 407 | 
            +
                  @lock = Mutex.new
         | 
| 408 | 
            +
                end
         | 
| 409 | 
            +
             | 
| 410 | 
            +
                attr_reader :site
         | 
| 411 | 
            +
             | 
| 412 | 
            +
                def compile_everything
         | 
| 413 | 
            +
                  # All compilation happens inside the same lock. So we shouldn't have
         | 
| 414 | 
            +
                  # to worry about deadlocks or anything
         | 
| 415 | 
            +
                  @lock.synchronize do
         | 
| 416 | 
            +
                    # Assets first since it's almost always a dependency of contents
         | 
| 417 | 
            +
                    compile_assets(site.representation.only_assets)
         | 
| 418 | 
            +
                    compile_contents(site.representation.only_contents)
         | 
| 419 | 
            +
                  end
         | 
| 420 | 
            +
                end
         | 
| 421 | 
            +
             | 
| 422 | 
            +
                private
         | 
| 423 | 
            +
             | 
| 424 | 
            +
                PARCEL_FILES_OUTPUT_REGEX = /^✨[^\n]+\n\n(.*)Done in(?:.*)\z/m
         | 
| 425 | 
            +
                PARCEL_FILE_OUTPUT_REGEX = /^(?<page>.*?)\s+(?<size>[0-9\.]+\s*[A-Z]?B)\s+(?<time>[0-9\.]+[a-z]?s)$/
         | 
| 426 | 
            +
             | 
| 427 | 
            +
                def compile_assets(assets)
         | 
| 428 | 
            +
                  if assets_dirty?
         | 
| 429 | 
            +
                    out, status = Open3.capture2(
         | 
| 430 | 
            +
                      "yarn",
         | 
| 431 | 
            +
                      "run",
         | 
| 432 | 
            +
                      "parcel",
         | 
| 433 | 
            +
                      "build",
         | 
| 434 | 
            +
                      "--dist-dir", site.assets_root.to_s,
         | 
| 435 | 
            +
                      "--public-url", site.assets_root.basename.to_s,
         | 
| 436 | 
            +
                      *assets.select(&:entrypoint?).map(&:input_path).map(&:to_s)
         | 
| 437 | 
            +
                    )
         | 
| 438 | 
            +
             | 
| 439 | 
            +
                    if !status.success?
         | 
| 440 | 
            +
                      raise "Asset compilation failed"
         | 
| 441 | 
            +
                    end
         | 
| 442 | 
            +
             | 
| 443 | 
            +
                    matches = PARCEL_FILES_OUTPUT_REGEX.match(out)[1]
         | 
| 444 | 
            +
             | 
| 445 | 
            +
                    if !matches
         | 
| 446 | 
            +
                      raise "Asset parsing failed"
         | 
| 447 | 
            +
                    end
         | 
| 448 | 
            +
             | 
| 449 | 
            +
                    processed_file_paths = matches.split("\n\n")
         | 
| 450 | 
            +
             | 
| 451 | 
            +
                    representation_by_input_path =
         | 
| 452 | 
            +
                      site.representation.only_assets.filter_map do |page|
         | 
| 453 | 
            +
                        next if page.input_path.nil?
         | 
| 454 | 
            +
             | 
| 455 | 
            +
                        [page.input_path, page]
         | 
| 456 | 
            +
                      end
         | 
| 457 | 
            +
                      .to_h
         | 
| 458 | 
            +
             | 
| 459 | 
            +
                    processed_file_paths.map do |file|
         | 
| 460 | 
            +
                      output_file, input_file = file.strip.split("\n└── ")
         | 
| 461 | 
            +
             | 
| 462 | 
            +
                      output_path = site.root.concat(output_file[PARCEL_FILE_OUTPUT_REGEX, 1])
         | 
| 463 | 
            +
                      input_path = site.root.concat(input_file[PARCEL_FILE_OUTPUT_REGEX, 1])
         | 
| 464 | 
            +
             | 
| 465 | 
            +
                      if !representation_by_input_path.key?(input_path)
         | 
| 466 | 
            +
                        next
         | 
| 467 | 
            +
                      end
         | 
| 468 | 
            +
             | 
| 469 | 
            +
                      url_path = output_path.relative_path_from(site.output_root)
         | 
| 470 | 
            +
                      representation_by_input_path[input_path].url_path = url_path.to_s
         | 
| 471 | 
            +
                      representation_by_input_path[input_path].output_path = output_path
         | 
| 472 | 
            +
                      representation_by_input_path[input_path].rendered_body = output_path.read
         | 
| 473 | 
            +
                    end
         | 
| 474 | 
            +
                  end
         | 
| 475 | 
            +
                end
         | 
| 476 | 
            +
             | 
| 477 | 
            +
                def compile_content(content)
         | 
| 478 | 
            +
                  if content.rendered_body
         | 
| 479 | 
            +
                    return
         | 
| 480 | 
            +
                  end
         | 
| 481 | 
            +
             | 
| 482 | 
            +
                  content_body = content.input_path.read
         | 
| 483 | 
            +
                  frontmatter, content_body = content_body.frontmatter_and_body
         | 
| 484 | 
            +
             | 
| 485 | 
            +
                  view_context = Rendering::ViewContext.new(
         | 
| 486 | 
            +
                    routes: site.representation.routes_getter,
         | 
| 487 | 
            +
                    layouts: site.representation.layouts_getter,
         | 
| 488 | 
            +
                    locals: content.locals,
         | 
| 489 | 
            +
                    frontmatter: frontmatter,
         | 
| 490 | 
            +
                    contents_path: site.contents_path,
         | 
| 491 | 
            +
                    layouts_path: site.layouts_path
         | 
| 492 | 
            +
                  )
         | 
| 493 | 
            +
             | 
| 494 | 
            +
                  content.rendered_body = view_context.render(
         | 
| 495 | 
            +
                    inline: content_body,
         | 
| 496 | 
            +
                    layout: content.layout,
         | 
| 497 | 
            +
                    renderers: content.renderers
         | 
| 498 | 
            +
                  )
         | 
| 499 | 
            +
                  content.rendered_body ||= ""
         | 
| 500 | 
            +
             | 
| 501 | 
            +
                  content.output_path =
         | 
| 502 | 
            +
                    if content.url_path[-1] == "/"
         | 
| 503 | 
            +
                      site.output_root.concat(content.url_path, site.config.fetch(:index))
         | 
| 504 | 
            +
                    else
         | 
| 505 | 
            +
                      site.output_root.concat(content.url_path)
         | 
| 506 | 
            +
                    end
         | 
| 507 | 
            +
             | 
| 508 | 
            +
                  if content.output_path.exist?
         | 
| 509 | 
            +
                    if content.output_path.read == content.rendered_body
         | 
| 510 | 
            +
                      return
         | 
| 511 | 
            +
                    end
         | 
| 512 | 
            +
                  end
         | 
| 513 | 
            +
             | 
| 514 | 
            +
                  if !site.output_root.include?(content.output_path)
         | 
| 515 | 
            +
                    return
         | 
| 516 | 
            +
                  end
         | 
| 517 | 
            +
             | 
| 518 | 
            +
                  if !content.output_path.dirname.directory?
         | 
| 519 | 
            +
                    content.output_path.dirname.mkpath
         | 
| 520 | 
            +
                  end
         | 
| 521 | 
            +
             | 
| 522 | 
            +
                  content.output_path.write(content.rendered_body)
         | 
| 523 | 
            +
             | 
| 524 | 
            +
                  nil
         | 
| 525 | 
            +
                end
         | 
| 526 | 
            +
             | 
| 527 | 
            +
                def compile_contents(contents)
         | 
| 528 | 
            +
                  return if !contents_dirty?
         | 
| 529 | 
            +
             | 
| 530 | 
            +
                  contents.each do |content|
         | 
| 531 | 
            +
                    compile_content(content)
         | 
| 532 | 
            +
                  end
         | 
| 533 | 
            +
                end
         | 
| 534 | 
            +
             | 
| 535 | 
            +
                private
         | 
| 536 | 
            +
             | 
| 537 | 
            +
                def contents_dirty?
         | 
| 538 | 
            +
                  true
         | 
| 539 | 
            +
                end
         | 
| 540 | 
            +
             | 
| 541 | 
            +
                def assets_dirty?
         | 
| 542 | 
            +
                  true
         | 
| 543 | 
            +
                end
         | 
| 544 | 
            +
              end
         | 
| 545 | 
            +
             | 
| 546 | 
            +
              class Publish
         | 
| 547 | 
            +
                def initialize(site)
         | 
| 548 | 
            +
                  @site = site
         | 
| 549 | 
            +
                  @compiler = HesitantCompiler.new(@site)
         | 
| 550 | 
            +
                end
         | 
| 551 | 
            +
             | 
| 552 | 
            +
                def write(path)
         | 
| 553 | 
            +
                  find_or_raise_or_mkdir(path)
         | 
| 554 | 
            +
             | 
| 555 | 
            +
                  @compiler.compile_everything
         | 
| 556 | 
            +
                end
         | 
| 557 | 
            +
             | 
| 558 | 
            +
                private
         | 
| 559 | 
            +
             | 
| 560 | 
            +
                def find_or_raise_or_mkdir(destination)
         | 
| 561 | 
            +
                  if !destination.exist?
         | 
| 562 | 
            +
                    if !destination.dirname.exist?
         | 
| 563 | 
            +
                      raise Error::PathDoesNotExist, "Destination's parent path does not exist: #{destination.dirname}"
         | 
| 564 | 
            +
                    end
         | 
| 565 | 
            +
             | 
| 566 | 
            +
                    destination.mkpath
         | 
| 567 | 
            +
                  end
         | 
| 568 | 
            +
                end
         | 
| 569 | 
            +
              end
         | 
| 570 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,129 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: confinement
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.0.1
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Zach Ahn
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: exe
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2020-03-29 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: bundler
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '2.0'
         | 
| 20 | 
            +
              type: :development
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - "~>"
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '2.0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: rake
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '13.0'
         | 
| 34 | 
            +
              type: :development
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '13.0'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: minitest
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - "~>"
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '5.0'
         | 
| 48 | 
            +
              type: :development
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - "~>"
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '5.0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: pry-byebug
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ">="
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
              type: :development
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - ">="
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: erubi
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - ">="
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '0'
         | 
| 76 | 
            +
              type: :runtime
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '0'
         | 
| 83 | 
            +
            description: 
         | 
| 84 | 
            +
            email:
         | 
| 85 | 
            +
            - engineering@zachahn.com
         | 
| 86 | 
            +
            executables:
         | 
| 87 | 
            +
            - confinement
         | 
| 88 | 
            +
            extensions: []
         | 
| 89 | 
            +
            extra_rdoc_files: []
         | 
| 90 | 
            +
            files:
         | 
| 91 | 
            +
            - ".gitignore"
         | 
| 92 | 
            +
            - ".ruby-version"
         | 
| 93 | 
            +
            - ".travis.yml"
         | 
| 94 | 
            +
            - Gemfile
         | 
| 95 | 
            +
            - Gemfile.lock
         | 
| 96 | 
            +
            - LICENSE.txt
         | 
| 97 | 
            +
            - README.md
         | 
| 98 | 
            +
            - Rakefile
         | 
| 99 | 
            +
            - bin/console
         | 
| 100 | 
            +
            - bin/rake
         | 
| 101 | 
            +
            - bin/setup
         | 
| 102 | 
            +
            - confinement.gemspec
         | 
| 103 | 
            +
            - exe/confinement
         | 
| 104 | 
            +
            - lib/confinement.rb
         | 
| 105 | 
            +
            - lib/confinement/version.rb
         | 
| 106 | 
            +
            homepage: https://github.com/zachahn/confinement
         | 
| 107 | 
            +
            licenses:
         | 
| 108 | 
            +
            - MIT
         | 
| 109 | 
            +
            metadata: {}
         | 
| 110 | 
            +
            post_install_message: 
         | 
| 111 | 
            +
            rdoc_options: []
         | 
| 112 | 
            +
            require_paths:
         | 
| 113 | 
            +
            - lib
         | 
| 114 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 115 | 
            +
              requirements:
         | 
| 116 | 
            +
              - - ">="
         | 
| 117 | 
            +
                - !ruby/object:Gem::Version
         | 
| 118 | 
            +
                  version: 2.7.0
         | 
| 119 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 120 | 
            +
              requirements:
         | 
| 121 | 
            +
              - - ">="
         | 
| 122 | 
            +
                - !ruby/object:Gem::Version
         | 
| 123 | 
            +
                  version: '0'
         | 
| 124 | 
            +
            requirements: []
         | 
| 125 | 
            +
            rubygems_version: 3.1.2
         | 
| 126 | 
            +
            signing_key: 
         | 
| 127 | 
            +
            specification_version: 4
         | 
| 128 | 
            +
            summary: Static site generator for when you're stuck at home
         | 
| 129 | 
            +
            test_files: []
         |