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 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
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.0
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.5
7
+ before_install: gem install bundler -v 1.17.3
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in confinement.gemspec
6
+ gemspec
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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
+
@@ -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
@@ -0,0 +1,3 @@
1
+ module Confinement
2
+ VERSION = "0.0.1"
3
+ 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: []