confinement 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []